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

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

MetaTrader 5Exemplos | 3 agosto 2020, 09:37
1 280 0
Stanislav Korotky
Stanislav Korotky

Na primeira parte desse artigo examinamos os princípios básicos que permitiam descrever o layout da interface gráfica de programas MQL em linguagem MQL. Para implementá-los, foi necessário criar várias classes diretamente responsáveis pela inicialização dos elementos da interface, combinando-os numa hierarquia comum e definindo propriedades. Agora estamos nos preparando para abordar exemplos mais complexos e, para não nos distrairmos com coisas complementares, voltaremos brevemente nossa atenção para a biblioteca de componentes padrão, que usaremos para construir os exemplos.

Customizando a biblioteca de controles padrão

Enquanto trabalhava na interface de janela vista nos artigos sobre OLAP anteriores, que também se baseavam na biblioteca padrão e nos contêineres CBox, precisávamos fazer algumas alterações nos componentes da biblioteca padrão. Como se viu, para integrar o sistema de layout proposto, é necessário ajustar ainda mais a biblioteca Controls — em parte em termos de expansão de recursos, em parte em termos de correção de erros. Nesse sentido, decidimos fazer uma cópia completa (ramificação de versão) de todas as classes, colocá-las na pasta ControlsPlus e depois trabalhar apenas com elas.

Aqui estão as principais mudanças.

Em quase todas as classes, o nível de acesso private foi alterado para protected para possibilitar a extensibilidade da biblioteca.

Para simplificar os projetos de depuração com elementos de GUI, à classe CWnd foi adicionado o campo de string _rtti e, no construtor de cada classe derivada, ele é preenchido com o nome de uma classe específica usando a macro RTTI.

  #define RTTI _rtti = StringFormat("%s %d", typename(this), &this);

Isso permite na janela do depurador ver a classe real dos objetos desreferenciados pelo link da classe base (o depurador, neste caso, mostra a classe base).

As informações sobre as margens e o alinhamento de elemento na classe CWnd são disponibilizadas usando dois novos métodos sobrecarregados. Além disso, agora é possível alterar o alinhamento e as margens separadamente.

    ENUM_WND_ALIGN_FLAGS Alignment(void) const
    {
      return (ENUM_WND_ALIGN_FLAGS)m_align_flags;
    }
    CRect Margins(void) const
    {
      CRectCreator rect(m_align_left, m_align_top, m_align_right, m_align_bottom);
      return rect;
    }
    void Alignment(const int flags)
    {
      m_align_flags = flags;
    }
    void Margins(const int left, const int top, const int right, const int bottom)
    {
      m_align_left = left;
      m_align_top = top;
      m_align_right = right;
      m_align_bottom = bottom;
    }

O método CWnd::Align foi reescrito para refletir o comportamento esperado de todos os modos de alinhamento. A implementação padrão não fornece um deslocamento para o limite da margem especificada, se for especificado o alongamento (as duas medidas são afetadas).

À classe CWndContainer é adicionado o método DeleteAll para excluir todos os elementos filho quando o contêiner for excluído. Ele é chamado desde Delete(CWnd *control) se o ponteiro para o "controle" passado contiver um objeto-contêiner.

Em vários locais da classe CWndClient são adicionadas strings para controlar a visibilidade da barra de rolagem, que pode ser alterada devido ao redimensionamento.

A classe CAppDialog agora leva em consideração o instance_id da janela ao atribuir identificadores aos elementos da interface. Sem essa alteração, os controles em janelas diferentes com o mesmo nome entrariam em conflito (afetariam um ao outro).

Nos grupos de "controles" — CRadioGroup, CCheckGroup, CListView — o método Redraw é virtual para que as classes herdadas "de borracha" possam responder corretamente ao redimensionamento. Além disso, o recálculo da largura dos elementos filho foi ligeiramente ajustado.

Às classes CDatePicker, CCheckBox e CRadioButton foi adicionado o método virtual OnResize para os mesmos fins. Na classe CDatePicker, foi corrigido um bug relacionado com a baixa prioridade do calendário flutuante (os cliques do mouse não tinham efeito nele).

O método CEdit::OnClick não "faz com que suma" o clique do mouse.

Além disso, anteriormente já foram desenvolvidas várias classes de "controles" que suportam redimensionamento e, dentro da estrutura deste projeto, foi expandido o número de classes "de borracha". Seus arquivos estão localizados na pasta Layouts.

  • ComboBoxResizable
  • SpinEditResizable
  • ListViewResizable
  • CheckGroupResizable
  • RadioGroupResizable

Devo lembrá-lo de que alguns "controles", como botão ou campo de entrada, suportam o alongamento nativo.

A estrutura geral da biblioteca de elementos padrão, levando em consideração as versões adaptadas com suporte para recipientes "de borracha" e de terceiros, é mostrada no diagrama de classes.

Hierarquia das classes dos controles

Hierarquia das classes dos controles


Geração de elementos e seu armazenamento em cache

Até agora, os elementos foram construídos como instâncias automáticas dentro do objeto de janela. Basicamente, são "lacunas" que são inicializadas por métodos como Create. O sistema de layout para elementos da GUI pode criar esses elementos, em vez de recebê-los da janela. Para fazer isso, basta ter algum tipo de armazenamento. Vamos chamá-lo de LayoutCache.

  template<typename C>
  class LayoutCache
  {
    protected:
      C *cache[];   // autocreated controls and boxes
      
    public:
      virtual void save(C *control)
      {
        const int n = ArraySize(cache);
        ArrayResize(cache, n + 1);
        cache[n] = control;
      }
      
      virtual C *get(const long m)
      {
        if(m < 0 || m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual C *get(const string name) = 0;
      virtual bool find(C *control);
      virtual int indexOf(C *control);
      virtual C *findParent(C *control) = 0;
      virtual bool revoke(C *control) = 0;
      virtual int cacheSize();
  };

De fato, é uma matriz de ponteiros de classe base (comuns a todos os elementos), onde eles podem ser colocados usando o método save. Além disso, a interface implementa (se possível neste nível abstrato) ou declara (para redefinição subsequente) métodos para localizar elementos por número, nome, link ou relações "pai" (feedback dos elementos aninhados para o contêiner).

Vamos adicionar o cache como um membro estático à classe LayoutBase.

  template<typename P,typename C>
  class LayoutBase: public LayoutData
  {
    protected:
      ...
      static LayoutCache<C> *cacher;
      
    public:
      static void setCache(LayoutCache<C> *c)
      {
        cacher = c;
      }

Cada janela terá que criar uma instância de cache para si mesma e configurá-la usando setCache no início do método, a semelhança de CreateLayout. Como os programas MQL são de thread único, temos a garantia de que as janelas (se forem necessárias várias) não se formarão em paralelo e competirão pelo ponteiro cacher. Limparemos o ponteiro automaticamente no destruidor LayoutBase, quando a pilha terminar — isso significa que deixamos o último contêiner externo na descrição do layout e não será necessário salvar mais nada.

      ~LayoutBase()
      {
        ...
        if(stack.size() == 0)
        {
          cacher = NULL;
        }
      }

A redefinição de referência não implica que estejamos limpando o cache. Simplesmente é assim que é garantido que o próximo potencial layout não adicione "controles" de outra janela por engano.

Para preencher o cache, vamos adicionar um novo tipo de método init ao LayoutBase — desta vez sem um ponteiro ou referência a um elemento da GUI de "terceiros" nos parâmetros.

      // nonbound layout, control T is implicitly stored in internal cache
      template<typename T>
      T *init(const string name, const int m = 1, const int x1 = 0, const int y1 = 0, const int x2 = 0, const int y2 = 0)
      {
        T *temp = NULL;
        for(int i = 0; i < m; i++)
        {
          temp = new T();
          if(save(temp))
          {
            init(temp, name + (m > 1 ? (string)(i + 1) : ""), x1, y1, x2, y2);
          }
          else return NULL;
        }
        return temp;
      }
      
      virtual bool save(C *control)
      {
        if(cacher != NULL)
        {
          cacher.save(control);
          return true;
        }
        return false;
      }

Graças ao modelo, temos a oportunidade de escrever um new T e gerar objetos no processo do layout (por padrão, 1 objeto por vez, mas, opcionalmente, podemos fazer vários).

Para os elementos da biblioteca padrão, foi escrita uma implementação de cache específica — StdLayoutCache (é fornecida aqui com abreviações, o código completo está no apêndice).

  // CWnd implementation specific!
  class StdLayoutCache: public LayoutCache<CWnd>
  {
    public:
      ...
      virtual CWnd *get(const long m) override
      {
        if(m < 0)
        {
          for(int i = 0; i < ArraySize(cache); i++)
          {
            if(cache[i].Id() == -m) return cache[i];
            CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
            if(container != NULL)
            {
              for(int j = 0; j < container.ControlsTotal(); j++)
              {
                if(container.Control(j).Id() == -m) return container.Control(j);
              }
            }
          }
          return NULL;
        }
        else if(m >= ArraySize(cache)) return NULL;
        return cache[(int)m];
      }
      
      virtual CWnd *findParent(CWnd *control) override
      {
        for(int i = 0; i < ArraySize(cache); i++)
        {
          CWndContainer *container = dynamic_cast<CWndContainer *>(cache[i]);
          if(container != NULL)
          {
            for(int j = 0; j < container.ControlsTotal(); j++)
            {
              if(container.Control(j) == control)
              {
                return container;
              }
            }
          }
        }
        return NULL;
      }
      ...
  };

Observe que o método get procura um "controle" por seu ordinal (se o parâmetro de entrada for positivo) ou por seu identificador (passado com um sinal de menos). Aqui, por identificador estamos nos referindo ao número exclusivo designado pela biblioteca de componentes padrão para envio de eventos. Em eventos, ele é passado no parâmetro lparam.

Na classe complementar da janela, podemos usar essa classe StdLayoutCache diretamente ou escrever uma derivada dela.

No seguinte exemplo veremos como o cache permite reduzir a descrição da classe de janela. Mas antes de prosseguir, examinemos alguns dos recursos adicionais que o cache fornece. Também os aplicaremos nos exemplos.

Estilizador

Como o cache é um objeto que processa itens centralmente, é bom usá-lo para muitas outras tarefas além do layout. Em particular, as regras de estilo único (cor, fonte, indentação) podem ser aplicadas uniformemente aos elementos. Ao mesmo tempo, basta configurar esse estilo num local, em vez de gravar as mesmas propriedades para cada "controle" separadamente. Além disso, o cache pode assumir a tarefa de processar mensagens para itens em cache. É possível que possamos construir dinamicamente, armazenar em cache e interagir com absolutamente todos os elementos. Assim, não precisaremos declarar nenhum elemento "explícito" na janela. Veremos mais adiante como os elementos criados dinamicamente se diferenciam favoravelmente dos elementos automáticos.

Para dar suporte a estilos centralizados, a classe StdLayoutCache fornece um método stub:

    virtual LayoutStyleable<C> *getStyler() const
    {
      return NULL;
    }

Se você não quiser usar estilos, não precisará codificar nada extra. No entanto, se entender os benefícios do gerenciamento centralizado de estilos, poderá implementar a classe herdeira LayoutStyleable. A interface é muito simples.

  enum STYLER_PHASE
  {
    STYLE_PHASE_BEFORE_INIT,
    STYLE_PHASE_AFTER_INIT
  };
  
  template<typename C>
  class LayoutStyleable
  {
    public:
      virtual void apply(C *control, const STYLER_PHASE phase) {};
  };

O método apply será chamado para cada "controle" duas vezes: no estágio de inicialização (STYLE_PHASE_BEFORE_INIT) e no estágio de registro no contêiner (STYLE_PHASE_AFTER_INIT). Assim, nos métodos LayoutBase::init é adicionada uma chamada no primeiro estágio:

      if(cacher != NULL)
      {
        LayoutStyleable<C> *styler = cacher.getStyler();
        if(styler != NULL)
        {
          styler.apply(object, STYLE_PHASE_BEFORE_INIT);
        }
      }

enquanto no destruidor são acrescentadas strings semelhantes, mas com STYLE_PHASE_AFTER_INIT para o segundo estágio.

As duas fases são necessárias porque os objetivos de estilo são diferentes. Para alguns elementos, às vezes precisamos definir propriedades individuais tendo maior prioridade sobre as gerais definidas no estilizador. No estágio de inicialização, o "controle" ainda está vazio (sem as configurações feitas no layout). No estágio de registro, todas as propriedades já estão definidas e, com base nas mesmas, podemos alterar o estilo. O exemplo mais óbvio é esse. Todos os campos de entrada com o sinalizador "somente leitura" devem ser exibidos em cinza. Mas a propriedade "somente leitura" é atribuída ao "controle" durante o processo de layout, após a inicialização e, portanto, o primeiro estágio aqui não é adequado e é necessário o segundo. Por outro lado, geralmente nem todos os campos de entrada terão esse sinalizador e, em todos os outros casos, é necessário definir a cor padrão antes que o idioma do layout execute uma personalização seletiva.

A propósito, pode ser usada uma abordagem semelhante para localização centralizada da interface dos programas MQL em diferentes idiomas.

Manipulação de eventos

A segunda função que é lógico delegar ao cache é a manipulação de eventos. Para eles na classe LayoutCache foi adicionado um método stub (C é o parâmetro do modelo de classe):

    virtual bool onEvent(const int event, C *control)
    {
      return false;
    }

Novamente, ele pode ser implementado numa classe derivada, mas não é obrigatório. Os códigos de eventos são específicos da biblioteca.

Para que esse método funcione, precisamos de definição de macros de interceptação de eventos semelhantes às da biblioteca padrão e escritas no mapa, por exemplo, assim:

  EVENT_MAP_BEGIN(Dialog)
    ON_EVENT(ON_CLICK, m_button1, OnClickButton1)
    ...
  EVENT_MAP_END(AppDialog)

Novas macros encaminham eventos para o objeto de cache. Eis um deles:

  #define ON_EVENT_LAYOUT_ARRAY(event, cache)  if(id == (event + CHARTEVENT_CUSTOM) && cache.onEvent(event, cache.get(-lparam))) { return true; }

Aqui vemos uma pesquisa dentro do cache pelo identificador chegando ao lparam (mas com o sinal oposto), após o qual o elemento encontrado é enviado para o manipulador onEvent discutido acima. Em princípio, não podemos procurar um elemento ao processar cada evento, mas, sim, lembrar-se do índice do elemento no cache no momento da gravação no cache e vincular um manipulador específico ao índice.

O tamanho atual do cache é o índice no qual o novo item é salvo. Podemos salvar o índice dos "controles" necessários durante o processo de layout.

          _layout<CButton> button1("Button");
          button1index = cache.cacheSize() - 1;

Aqui button1index é uma variável inteira na classe janela. Deve ser usada em outra macro definida para processar itens por índice de cache:

  #define ON_EVENT_LAYOUT_INDEX(event, cache, controlIndex, handler)  if(id == (event + CHARTEVENT_CUSTOM) && lparam == cache.get(controlIndex).Id()) { handler(); return(true); }

Além disso, podemos enviar eventos não para o cache, mas, sim, diretamente para os próprios elementos. Para isso, o elemento deve implementar a interface Notifiable, modelada pela classe de "controle" necessária.

  template<typename C>
  class Notifiable: public C
  {
    public:
      virtual bool onEvent(const int event, void *parent) = 0;
  };

Qualquer objeto pode ser passado no parâmetro parent, incluindo uma caixa de diálogo. Com base em Notifiable, por exemplo, é fácil criar um botão que herda do CButton.

  class NotifiableButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        this.StateFlagsReset(7);
        return true;
      }
  };

Existem 2 macros para trabalhar com elementos "notificáveis". Sua diferença está apenas no número de parâmetros: ON_EVENT_LAYOUT_CTRL_ANY permite passar um objeto arbitrário pelos últimos parâmetros, e ON_EVENT_LAYOUT_CTRL_DLG não possui esse parâmetro, porque sempre envia this da caixa de diálogo como um objeto.

  #define ON_EVENT_LAYOUT_CTRL_ANY(event, cache, type, anything)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, anything)) { return true; }}
  #define ON_EVENT_LAYOUT_CTRL_DLG(event, cache, type)  if(id == (event + CHARTEVENT_CUSTOM)) {type *ptr = dynamic_cast<type *>(cache.get(-lparam)); if(ptr != NULL && ptr.onEvent(event, &this)) { return true; }}

Veremos diferentes opções para manipular eventos no contexto do segundo exemplo.

Exemplo 2. Diálogo com controles

O projeto demo contém a classe CControlsDialog com os principais tipos de "controles" da Biblioteca Padrão. Por analogia com o primeiro exemplo, removeremos todos os métodos de criação e os substituiremos pelo único CreateLayout. A propósito, havia até 17 desses métodos no projeto antigo, e eles foram chamados entre si usando operadores condicionais complexos.

Para que, ao gerar "controles", eles possam ser salvos no cache, vamos adicionar uma classe de cache simples e, ao mesmo tempo, uma classe de estilo. Vamos mostrar o cache primeiro.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      MyLayoutStyleable styler;
      CControlsDialog *parent;
      
    public:
      MyStdLayoutCache(CControlsDialog *owner): parent(owner) {}
      
      virtual StdLayoutStyleable *getStyler() const override
      {
        return (StdLayoutStyleable *)&styler;
      }
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          parent.SetCallbackText(__FUNCTION__ + " " + control.Name());
          return true;
        }
        return false;
      }
  };

Na classe de cache é declarado o manipulador de eventos onEvent, que anexaremos através do mapa de eventos. Aqui, o manipulador envia uma mensagem para a janela pai, onde, como nas versões anteriores do exemplo, é exibida no campo de informações.

A classe de estilizador fornece a configuração dos mesmos campos para todos os elementos, uma fonte não padrão em todos os botões, além de exibir o CEdit com o atributo "somente leitura" em cinza (temos um, mas, se adicionado, ele se enquadra automaticamente na configuração geral).

  class MyLayoutStyleable: public StdLayoutStyleable
  {
    public:
      virtual void apply(CWnd *control, const STYLER_PHASE phase) override
      {
        CButton *button = dynamic_cast<CButton *>(control);
        if(button != NULL)
        {
          if(phase == STYLE_PHASE_BEFORE_INIT)
          {
            button.Font("Arial Black");
          }
        }
        else
        {
          CEdit *edit = dynamic_cast<CEdit *>(control);
          if(edit != NULL && edit.ReadOnly())
          {
            if(phase == STYLE_PHASE_AFTER_INIT)
            {
              edit.ColorBackground(clrLightGray);
            }
          }
        }
        
        if(phase == STYLE_PHASE_BEFORE_INIT)
        {
          control.Margins(DEFAULT_MARGIN);
        }
      }
  };

A referência para o cache é armazenada na janela, é criado e excluído, respectivamente, no construtor e no destruidor e, quando criado, uma referência é passado para a janela como parâmetro para dar feedback.

  class CControlsDialog: public AppDialogResizable
  {
    private:
      ...
      MyStdLayoutCache *cache;
    public:
      CControlsDialog(void)
      {
        cache = new MyStdLayoutCache(&this);
      }

Agora vamos ver o método CreateLayout em ordem. Ao ler as descrições detalhadas, pode parecer que o método é muito longo e complicado, mas, na realidade, não é. Se removermos os comentários cognitivos (que não estarão num projeto real), o método caberá numa tela e não conterá lógica complexa.

No início, o cache é ativado chamando setCache. Em seguida, o primeiro bloco descreve o contêiner principal CControlsDialog. Ele não estará no cache porque estamos passando uma referência ao já criado this.

  bool CControlsDialog::CreateLayout(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    StdLayoutBase::setCache(cache); // assign the cache object to store implicit objects
    
    {
      _layout<CControlsDialog> dialog(this, name, x1, y1, x2, y2);

Em seguida, uma instância implícita do contêiner aninhado da classe CBox é criada para a área do cliente da janela. Sua orientação é vertical, portanto, os contêineres aninhados preencherão o espaço de cima para baixo. Armazenamos a referência do objeto na variável m_main porque precisamos chamar o método Pack depois que a janela é redimensionada. Se o seu diálogo não for de borracha, isso não será necessário. Finalmente, a área do cliente é definida como zero margens e um alinhamento em todas as bordas, para que o painel preencha toda a janela mesmo após o redimensionamento.

      {
        // example of implicit object in the cache
        _layout<CBox> clientArea("main", ClientAreaWidth(), ClientAreaHeight(), LAYOUT_STYLE_VERTICAL);
        m_main = clientArea.get(); // we can get the pointer to the object from cache (if required)
        clientArea <= WND_ALIGN_CLIENT <= 0.0; // double type is important

No próximo nível, o primeiro é o contêiner, que ocupará toda a largura da janela, mas será um pouco maior que o campo de entrada. Além disso, com ajuda do alinhamento WND_ALIGN_TOP (além de WND_ALIGN_WIDTH), ele será "colado" na borda superior da janela.

        {
          // another implicit container (we need no access it directly)
          _layout<CBox> editRow("editrow", ClientAreaWidth(), EDIT_HEIGHT * 1.5, (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH));

Dentro está o único "controle" da classe CEdit no modo "somente leitura". Como um variável m_edit explícita é reservada para ela, não entrará no cache.

          {
            // for editboxes default boolean property is ReadOnly
            _layout<CEdit> edit(m_edit, "Edit", ClientAreaWidth(), EDIT_HEIGHT, true);
          }
        }

Neste ponto, inicializamos 3 elementos. Após o parêntese de fechamento, o objeto de layout edit será destruído e, durante a execução de seu destruidor, m_edit será adicionado ao contêiner "editrow". Mas aqui segue outro parêntese de fechamento. Ele destrói o contexto em que "viveu" o objeto de layout editRow e, portanto, esse contêiner é adicionado ao contêiner da área do cliente que permanece na pilha. Assim, é formada a primeira linha do layout vertical em m_main.

Em seguida, temos uma fila com três botões. Primeiro, um contêiner é criado com base nela.

        {
          _layout<CBox> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          buttonRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);

Aqui vale a pena prestar atenção à maneira não padronizada de alinhamento WND_ALIGN_CONTENT. Isso significa o seguinte.

Um algoritmo para dimensionar elementos aninhados para ajustar o tamanho do contêiner foi adicionado à classe CBox. É executado no método AdjustFlexControls e entra em vigor apenas se nos sinalizadores de alinhamento do contêiner for especificado o valor especial WND_ALIGN_CONTENT. Ele não faz parte da enumeração ENUM_WND_ALIGN_FLAGS padrão. O contêiner analisa os "controles" para ver quais deles têm um tamanho fixo e quais não. Os "controles" de tamanho fixo são aqueles que não especificam o alinhamento de acordo com os lados do contêiner (numa dimensão específica). Para todos esses "controles", o contêiner calcula a soma de seus tamanhos, soma essa que subtrai do tamanho total do contêiner e divide o restante proporcionalmente entre todos os demais "controles". Por exemplo, se houver dois "controles" num contêiner e nenhum deles tiver uma ligação, eles dividirão a área inteira do contêiner pela metade.

Esse é um modo muito conveniente, mas não deve ser abusado em muitos contêineres aninhados, por causa do algoritmo de dimensionamento de passagem única, cujo alinhamento de elementos internos ao longo da área do contêiner se ajusta ao conteúdo, gera incerteza (por esse motivo, as classes de layout incluem o evento especial ON_LAYOUT_REFRESH que a janela pode enviar para si mesma para repetir o cálculo do tamanho).

No caso de nossa fila com três botões, todos eles mudarão de comprimento proporcionalmente quando a largura da janela mudar. O primeiro botão da classe CButton é criado implicitamente e armazenado no cache.

          { // 1
            _layout<CButton> button1("Button1");
            button1index = cache.cacheSize() - 1;
            button1["width"] <= BUTTON_WIDTH;
            button1["height"] <= BUTTON_HEIGHT;
          } // 1

O segundo botão tem uma classe NotifiableButton (já descrita acima). O botão processará as próprias mensagens.

          { // 2
            _layout<NotifiableButton> button2("Button2", BUTTON_WIDTH, BUTTON_HEIGHT);
          } // 2

O terceiro botão é criado com base na variável de janela explicitamente definida m_button3 e possui a propriedade "aderência".

          { // 3
            _layout<CButton> button3(m_button3, "Button3", BUTTON_WIDTH, BUTTON_HEIGHT, "Locked");
            button3 <= true; // for buttons default boolean property is Locking
          } // 3
        }

Observe que todos os botões são cercados por seus próprios colchetes. Por esse motivo, eles são adicionados à linha na ordem em que os parênteses de fechamento marcados 1, 2, 3 ocorrem, ou seja, na ordem natural. Poderíamos ter pulado esses blocos "pessoais" para cada botão e ter-nos limitado a um bloco de contêiner comum. Mas, nesse caso, os botões seriam adicionados na ordem inversa, porque os destruidores de objetos são sempre chamados na ordem inversa desde a sua criação. Seria possível "consertar" a situação invertendo a ordem da descrição dos botões no layout.

A terceira fila contém um contêiner com controles giratórios e um calendário. O contêiner é criado "anonimamente" e armazenado no cache.

        {
          _layout<CBox> spinDateRow("spindaterow", ClientAreaWidth(), BUTTON_HEIGHT * 1.5);
          spinDateRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_WIDTH);
          
          {
            _layout<SpinEditResizable> spin(m_spin_edit, "SpinEdit", GROUP_WIDTH, EDIT_HEIGHT);
            spin["min"] <= 10;
            spin["max"] <= 1000;
            spin["value"] <= 100; // can set value only after limits (this is how SpinEdits work)
          }
          
          {
            _layout<CDatePicker> date(m_date, "Date", GROUP_WIDTH, EDIT_HEIGHT, TimeCurrent());
          }
        }

Finalmente, o último contêiner ocupa o restante da janela e contém duas colunas de itens. As cores brilhantes são atribuídas apenas para ver onde está o contêiner na janela.

        {
          _layout<CBox> listRow("listsrow", ClientAreaWidth(), LIST_HEIGHT);
          listRow["top"] <= (int)(EDIT_HEIGHT * 1.5 * 3);
          listRow["align"] <= (WND_ALIGN_CONTENT|WND_ALIGN_CLIENT);
          (listRow <= clrMagenta)["border"] <= clrBlue;
          
          createSubList(&m_lists_column1, LIST_OF_OPTIONS);
          createSubList(&m_lists_column2, LIST_LISTVIEW);
          // or vice versa (changed order gives swapped left/right side location)
          // createSubList(&m_lists_column1, LIST_LISTVIEW);
          // createSubList(&m_lists_column2, LIST_OF_OPTIONS);
        }

Deve-se notar especialmente aqui que as duas colunas m_lists_column1 e m_lists_column2 são preenchidas não no próprio método CreateLayout, mas, sim, usando o método auxiliar createSubList. Do ponto de vista do layout, chamar uma função não é diferente de inserir outro bloco entre parênteses. Isso significa que o layout não precisa consistir numa lista estática longa, mas, sim, pode incluir fragmentos de variáveis condicionais. Também podemos incluir o mesmo fragmento em diferentes diálogos.

No nosso exemplo, se mudarmos o segundo parâmetro da função, podemos alterar a ordem das colunas na janela.

      }
    }

Após fechar todos os colchetes, todos os elementos da GUI são inicializados e anexados uns aos outros. Chamamos o método Pack (diretamente ou através de SelfAdjustment, onde ele também é chamado em resposta a uma solicitação do diálogo "de borracha").

    // m_main.Pack();
    SelfAdjustment();
    return true;
  }

Não entraremos em detalhes sobre o método createSubList. Dentro são implementados recursos para gerar um conjunto de 3 "controles" (cobmobox, um grupo de opções e um grupo de botões de opção) ou uma lista (ListView), tudo na versão "de borracha". O interessante é que o preenchimento de "controles" é realizado com ajuda de mais outra classe de geradores ItemGenerator.

  template<typename T>
  class ItemGenerator
  {
    public:
      virtual bool addItemTo(T *object) = 0;
  };

O único método dessa classe é chamado desde o layout do "controle" object até que o método retorne false (sinal de fim de dados).

Por padrão, são fornecidos vários geradores simples para a biblioteca padrão (eles usam o método dos "controles" AddItem): StdItemGenerator, StdGroupItemGenerator, SymbolsItemGenerator, ArrayItemGenerator. Em particular, o SymbolsItemGenerator permite que preencher o "controle" com símbolos da "Observação do Mercado".

  template<typename T>
  class SymbolsItemGenerator: public ItemGenerator<T>
  {
    protected:
      long index;
      
    public:
      SymbolsItemGenerator(): index(0) {}
      
      virtual bool addItemTo(T *object) override
      {
        object.AddItem(SymbolName((int)index, true), index);
        index++;
        return index < SymbolsTotal(true);
      }
  };

No layout, é especificado da mesma maneira que os geradores de "controles". Como alternativa, é permitido passar para o objeto de layout não uma referência a um objeto gerador automático ou estático (que deve ser descrito no código em algum lugar previamente), mas, sim, como um ponteiro para um objeto alocado dinamicamente.

        _layout<ListViewResizable> list(m_list_view, "ListView", GROUP_WIDTH, LIST_HEIGHT);
        list <= WND_ALIGN_CLIENT < new SymbolsItemGenerator<ListViewResizable>();

O operador < é usado para isso. O gerador alocado dinamicamente será excluído automaticamente após o desligamento.

Para anexar novos eventos, ao mapa são adicionadas as macros correspondentes.

  EVENT_MAP_BEGIN(CControlsDialog)
    ...
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, NotifiableButton)
    ON_EVENT_LAYOUT_INDEX(ON_CLICK, cache, button1index, OnClickButton1)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
  EVENT_MAP_END(AppDialogResizable)

A macro ON_EVENT_LAYOUT_CTRL_DLG anexa notificações sobre cliques do mouse para qualquer botão da classe NotifiableButton (no nosso caso, é um). A macro ON_EVENT_LAYOUT_INDEX despacha o mesmo evento para o botão com o índice especificado no cache. Mas essa macro podia não ter sido gravada, porque a última linha da macro ON_EVENT_LAYOUT_ARRAY encaminhará o clique do mouse para qualquer elemento no cache se o identificador lparam corresponder.

Em princípio, foi possível transferir todos os elementos para o cache e processar seus eventos de uma nova maneira, mas a forma anterior também funciona e elas podem ser combinadas.

A imagem animada a seguir mostra a reação aos eventos.

Diálogo com controles, formado usando a linguagem de marcação MQL

Diálogo com controles, formado usando a linguagem de marcação MQL

Observe que a maneira como o evento é transmitido pode ser indiretamente determinada pela assinatura da função exibida no campo de informações. Também pode-se ver que os eventos são enviados não apenas para "controles", mas também para contêineres. As caixas vermelhas dos contêineres são exibidas para depuração. Elas podem ser desabilitadas usando a macro LAYOUT_BOX_DEBUG.

Exemplo 3. Layouts dinâmicos DynamicForm

Neste exemplo final, veremos um formulário no qual todos os itens serão criados dinamicamente no cache. Isso nos dará alguns novos recursos importantes.

Como no exemplo anterior, o cache suportará o estilo dos elementos. A única configuração de estilo são os mesmos campos perceptíveis, que permitem ver o aninhamento de contêineres e selecioná-los à vontade com o mouse.

Dentro do método CreateLayout, é descrita a seguinte estrutura de interface simples. O contêiner principal, como sempre, ocupa toda a área do cliente da janela. Na parte superior, há um bloco com dois botões: Inject e Export. Todo o espaço abaixo dele é ocupado por um contêiner, dividido em colunas da esquerda e da direita. A coluna da esquerda, marcada em cinza, está inicialmente vazia. Na coluna da direita, há um grupo de botões de opção que permite selecionar o tipo de controle.

      {
        // example of implicit object in the cache
        _layout<CBoxV> clientArea("main", ClientAreaWidth(), ClientAreaHeight());
        m_main = clientArea.get();
        clientArea <= WND_ALIGN_CLIENT <= PackedRect(10, 10, 10, 10);
        clientArea["background"] <= clrYellow <= VERTICAL_ALIGN_TOP;
        
        {
          _layout<CBoxH> buttonRow("buttonrow", ClientAreaWidth(), BUTTON_HEIGHT * 5);
          buttonRow <= 5.0 <= (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_TOP|WND_ALIGN_WIDTH);
          buttonRow["background"] <= clrCyan;
          
          {
            // these 2 buttons will be rendered in reverse order (destruction order)
            // NB: automatic variable m_button3
            _layout<CButton> button3(m_button3, "Export", BUTTON_WIDTH, BUTTON_HEIGHT);
            _layout<NotifiableButton> button2("Inject", BUTTON_WIDTH, BUTTON_HEIGHT);
          }
        }
        
        {
          _layout<CBoxH> buttonRow("buttonrow2", ClientAreaWidth(), ClientAreaHeight(),
            (ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_CONTENT|WND_ALIGN_CLIENT));
          buttonRow["top"] <= BUTTON_HEIGHT * 5;
          
          {
            {
              _layout<CBoxV> column("column1", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
              column <= clrGray;
              {
                // dynamically created controls will be injected here
              }
            }
            
            {
              _layout<CBoxH> column("column2", GROUP_WIDTH, 100, WND_ALIGN_HEIGHT);
            
              _layout<RadioGroupResizable> selector("selector", GROUP_WIDTH, CHECK_HEIGHT);
              selector <= WND_ALIGN_HEIGHT;
              string types[3] = {"Button", "CheckBox", "Edit"};
              ArrayItemGenerator<RadioGroupResizable,string> ctrls(types);
              selector <= ctrls;
            }
          }
        }
      }

Supõe-se que, após selecionar o tipo de elemento no grupo de rádio, o usuário clique no botão Inject e o "controle" correspondente seja criado na parte esquerda da janela. Obviamente, podemos criar sequencialmente vários "controles" diferentes. Eles serão centralizados automaticamente de acordo com as configurações do contêiner. Para implementar essa lógica, o botão Inject possui uma classe NotifiableButton com um manipulador onEvent.

  class NotifiableButton: public Notifiable<CButton>
  {
      static int count;
      
      StdLayoutBase *getPtr(const int value)
      {
        switch(value)
        {
          case 0:
            return new _layout<CButton>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 1:
            return new _layout<CCheckBox>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
          case 2:
            return new _layout<CEdit>("More" + (string)count++, BUTTON_WIDTH, BUTTON_HEIGHT);
        }
        return NULL;
      }
      
    public:
      virtual bool onEvent(const int event, void *anything) override
      {
        DynamicForm *parent = dynamic_cast<DynamicForm *>(anything);
        MyStdLayoutCache *cache = parent.getCache();
        StdLayoutBase::setCache(cache);
        CBox *box = cache.get("column1");
        if(box != NULL)
        {
          // put target box to the stack by retrieving it from the cache
          _layout<CBox> injectionPanel(box, box.Name());
          
          {
            CRadioGroup *selector = cache.get("selector");
            if(selector != NULL)
            {
              const int value = (int)selector.Value();
              if(value != -1)
              {
                AutoPtr<StdLayoutBase> base(getPtr(value));
                (~base).get().Id(rand() + (rand() << 32));
              }
            }
          }
          box.Pack();
        }
        
        return true;
      }
  };

O contêiner no qual novos itens devem ser inseridos é procurado primeiro no cache chamado "column1". Este contêiner é o primeiro parâmetro ao criar o objeto InjectionPanel. O fato de o elemento a ser transferido já estar no cache é especialmente levado em consideração no algoritmo de layout, pois ele não é adicionado ao cache novamente, mas é empurrado para a pilha de contêineres como de costume. Isso fornece a possibilidade de adicionar elementos a contêineres "antigos".

Com base na escolha do usuário, o objeto do tipo necessário é criado com ajuda do novo operador no método auxiliar getPtr. Para que os "controles" adicionados funcionem normalmente, para eles são gerados aleatoriamente identificadores exclusivos. A remoção de ponteiro ao sair de um bloco de código é garantida pela classe especial AutoPtr.

Se adicionarmos muitos elementos, eles ficarão fora do contêiner. Isso acontece porque as nossas classes de contêineres ainda não têm sabem responder adequadamente a estouros. Neste caso, seria possível, por exemplo, mostrar a barra de rolagem e ocultar os elementos salientes da borda.

Mas isso não importa. O ponto do exemplo é que podemos gerar conteúdo dinâmico, personalizando o formulário e dando aos contêineres o preenchimento e o tamanho necessários.

Além de adicionar elementos, dado diálogo também pode excluí-los. Qualquer elemento no formulário pode ser selecionado com um clique do mouse. Além disso, a classe e o nome do elemento são exibidos no log, enquanto o próprio elemento é destacado com uma borda vermelha. Se clicarmos num elemento já selecionado, a caixa de diálogo solicitará que confirmemos a exclusão e, se concordarmos, excluirá o elemento. Tudo isso é implementado em nossa classe de cache.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      DynamicForm *parent;
      CWnd *selected;
      
      bool highlight(CWnd *control, const color clr)
      {
        CWndObj *obj = dynamic_cast<CWndObj *>(control);
        if(obj != NULL)
        {
          obj.ColorBorder(clr);
          return true;
        }
        else
        {
          CWndClient *client = dynamic_cast<CWndClient *>(control);
          if(client != NULL)
          {
            client.ColorBorder(clr);
            return true;
          }
        }
        return false;
      }
      
    public:
      MyStdLayoutCache(DynamicForm *owner): parent(owner) {}
      
      virtual bool onEvent(const int event, CWnd *control) override
      {
        if(control != NULL)
        {
          highlight(selected, CONTROLS_BUTTON_COLOR_BORDER);
          
          CWnd *element = control;
          if(!find(element)) // this is an auxiliary object, not a compound control
          {
            element = findParent(control); // get actual GUI element
          }
          
          if(element == NULL)
          {
            Print("Can't find GUI element for ", control._rtti + " / " + control.Name());
            return true;
          }
          
          if(selected == control)
          {
            if(MessageBox("Delete " + element._rtti + " / " + element.Name() + "?", "Confirm", MB_OKCANCEL) == IDOK)
            {
              CWndContainer *container;
              container = dynamic_cast<CWndContainer *>(findParent(element));
              if(container)
              {
                revoke(element); // deep remove of all references (with subtree) from cache
                container.Delete(element); // delete all subtree of wnd-objects
                
                CBox *box = dynamic_cast<CBox *>(container);
                if(box) box.Pack();
              }
              selected = NULL;
              return true;
            }
          }
          selected = control;
          
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", b);
          
          return true;
        }
        return false;
      }
  };

Podemos excluir qualquer elemento da interface que esteja no cache, ou seja, não apenas os adicionados pelo botão Inject. Dessa forma, podemos, por exemplo, excluir toda a metade esquerda ou a "caixa de rádio" direita. Mas a coisa mais interessante acontece se tentarmos remover o recipiente superior com dois botões. Como resultado, o botão Export perderá a vinculação com a caixa de diálogo e permanecerá no gráfico.

Formulário editável: adição e remoção de elementos

Formulário editável: adição e remoção de elementos

Isso acontece porque este é o único elemento que é intencionalmente descrito como uma variável automática, não uma variável dinâmica (a classe do formulário possui uma instância CButton m_button3).

Quando a biblioteca padrão tenta remover elementos da interface, ela delega isso à classe de matrizes CArrayObj, que por sua vez verifica o tipo de ponteiro e remove apenas objetos com POINTER_DYNAMIC. Assim, torna-se óbvio que, para a construção de uma interface responsiva, na qual os elementos possam se substituir ou serem completamente removidos, é desejável usar posicionamento dinâmico, e o cache oferece uma solução pronta para isso.

Por fim, vamos passar para o segundo botão da caixa de diálogo — Export. Como podemos imaginar pelo nome, ele foi projetado para salvar o estado atual da caixa de diálogo como um arquivo de texto com base na sintaxe de layouts MQL considerada. Certamente, o formulário permite que personalizemos sua aparência apenas de forma limitada, para fins de demonstração, mas a própria capacidade de descarregar a aparência num código MQL pronto, que pode ser facilmente copiado num programa e obter a mesma interface, é uma abordagem potencialmente valiosa. Obviamente, apenas a interface é migrada e o código de manipulação de eventos ou as configurações gerais do estilizador devem ser incluídas por conta própria.

A exportação é fornecida pela classe LayoutExporter, não a consideraremos em detalhes, os códigos-fonte estão anexados.

Fim do artigo

Neste artigo, testamos a viabilidade da ideia de descrever o layout da interface gráfica de programas MQL na própria linguagem MQL. O uso da geração dinâmica de itens com armazenamento em cache centralizado simplifica a criação e o gerenciamento de uma hierarquia de componentes. O cache pode ser usado para realizar a maioria das tarefas associadas ao design da interface, em particular alterações de estilo, manipulação de eventos, edição de layout em tempo real e salvamento num formato adequado para uso posterior.

Se juntarmos essas funções, descobriremos que temos quase tudo para um simples editor visual de formulários. Ele poderia suportar apenas as propriedades mais importantes, comuns a muitos "controles", no entanto, permitiria gerar modelos de interface. Porém, vimos que mesmo o estágio inicial para avaliar esse novo conceito exigia muito esforço. Por isso, implementar um editor completo na prática é uma tarefa bastante difícil. E isso é outra história.

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

Arquivos anexados |
MQL5GUI2.zip (98.72 KB)
Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático Otimização Walk Forward contínua (parte 6): Lógica e estrutura do otimizador automático
Anteriormente, nós consideramos a criação da otimização walk forward automática. Desta vez, nós prosseguiremos para a estrutura interna da ferramenta de otimização automática. O artigo será útil para todos aqueles que desejam continuar trabalhando com o projeto criado e modificá-lo, bem como para aqueles que desejam entender a lógica do programa. O artigo atual contém diagramas UML que apresentam a estrutura interna do projeto e os relacionamentos entre seus objetos. Ele também descreve o processo de início da otimização, mas não contém a descrição do processo de implementação do otimizador.
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 40): indicadores com base na biblioteca - atualização de dados em tempo real Trabalhando com séries temporais na biblioteca DoEasy (Parte 40): indicadores com base na biblioteca - atualização de dados em tempo real
Neste artigo, consideraremos a criação de um indicador multiperíodo simples com base na biblioteca DoEasy. Modificaremos as classes de séries temporais para receber dados de qualquer timeframe e exibi-los no período gráfico atual.
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.