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

Linguagem MQL como um meio de marcação da interface gráfica de programas MQL (Parte 3). Designer de formulários

MetaTrader 5Exemplos | 7 setembro 2020, 10:47
1 380 0
Stanislav Korotky
Stanislav Korotky

Nos primeiros dois artigos (1, 2) examinamos o conceito geral de construção de um sistema de layout de interface em linguagem MQL e a implementação das classes principais que facilitam inicialização hierárquica de elementos de interface, armazenamento em cache, estilo, propriedades de configuração e tratamento de eventos. A criação dinâmica de elementos a pedido torna possível modificar o layout de uma caixa de diálogo simples em tempo real, enquanto um único repositório de elementos naturalmente torna possível salvá-la na sintaxe MQL proposta para subsequente inserção "tal como está" num programa MQL que esteja precisando de GUI. Se considerarmos esse antecedentes, estamos perto de criar um editor de formulários gráfico. Neste artigo, abordaremos de perto essa tarefa.

Formulação do problema

O editor deve garantir a colocação dos elementos na janela e a configuração de suas propriedades básicas. A seguir está uma lista geral contendo as propriedades que serão suportadas, mas nem todas elas estão presentes em todos os tipos de elementos.

  • tipo,
  • nome,
  • largura,
  • altura,
  • estilo de alinhamento de conteúdo interno,
  • texto ou título,
  • cor de fundo,
  • alinhamento no contêiner pai,
  • recuo/margens das bordas do contêiner.

Aqui faltam muitas outras propriedades, por exemplo, o nome e o tamanho da fonte, propriedades específicas de vários tipos de "controles" (em particular, a propriedade "fixação" de botões). Isso é feito deliberadamente para simplificar um projeto cuja missão principal é a de provar um conceito (proof of concept, POC). Se necessário, é possível adicionar suporte para propriedades adicionais ao editor posteriormente.

O posicionamento em coordenadas absolutas está disponível indiretamente por meio de recuos, mas não é recomendado. O uso de contêineres CBox pressupõe que o posicionamento deve ser feito automaticamente pelos próprios contêineres de acordo com as configurações de alinhamento.

O editor é projetado para classes de elementos de interface da Biblioteca Padrão. Para criar ferramentas semelhantes para outras bibliotecas, precisaremos escrever implementações concretas de todas as entidades abstratas desde o sistema de layout proposto. Adicionalmente, devemos nos orientar pela implementação de classes de layout para a Biblioteca Padrão.

Deve-se notar que o nome "biblioteca de componentes padrão" não corresponde exatamente à realidade, uma vez que no contexto de artigos anteriores já tivemos que modificá-la significativamente e exibi-la como uma ramificação de versão paralela na pasta ControlsPlus. Para os fins deste artigo, continuaremos a usá-la e modificá-la.

Vamos listar os tipos de elementos que o editor suportará.

  • contêineres CBox com orientação horizontal (CBoxH) e vertical (CBoxV),
  • botão CButton,
  • campo de entrada CEdit,
  • rótulo CLabel,
  • campo de entrada com iteração de valores SpinEditResizable,
  • calendário CDatePicker,
  • lista suspensa ComboBoxResizable,
  • lista ListViewResizable,
  • grupo de botões de opção independentes CheckGroupResizable,
  • grupo de botões de opção RadioGroupResizable.

Todas as classes facilitam o redimensionamento adaptável (alguns tipos padrão tinham isso desde o início, outros receberam modificações significativas).

O programa consistirá em duas janelas: caixa de diálogo "Inspetor", na qual o usuário seleciona as propriedades dos controles criados, e o formulário Designer, no qual esses elementos são criados, o que dá a aparência à interface gráfica projetada.

Esboço da interface do programa-designer GUI MQL

Esboço da interface do programa-designer GUI MQL

Do ponto de vista de MQL, o programa terá 2 classes principais - InspectorDialog e DesignerForm - descritas nos arquivos de cabeçalho de mesmo nome.

  #include "InspectorDialog.mqh"
  #include "DesignerForm.mqh"
  
  InspectorDialog inspector;
  DesignerForm designer;
  
  int OnInit()
  {
      if(!inspector.CreateLayout(0, "Inspector", 0, 20, 20, 200, 400)) return (INIT_FAILED);
      if(!inspector.Run()) return (INIT_FAILED);
      if(!designer.CreateLayout(0, "Designer", 0, 300, 50, 500, 300)) return (INIT_FAILED);
      if(!designer.Run()) return (INIT_FAILED);
      return (INIT_SUCCEEDED);
  }

Ambas as janelas são herdeiras de AppDialogResizable (doravante CAppDialog), criadas com a abordagem de marcação MQL. Por isso, vemos uma chamada para CreateLayout em vez de Create.

Cada janela tem seu próprio cache de elementos de interface. No entanto, no inspetor ele é preenchido desde o início com "controles" descritos num layout bastante complicado (que tentaremos considerar em termos gerais), já no designer ele está vazio. A explicação para isso é simples, quase toda a lógica de negócios do programa está ligada ao inspetor, enquanto o designer é um vazio, no qual o inspetor irá gradualmente, de acordo com os comandos do usuário, introduzir novos elementos.

Conjunto de propriedades PropertySet

Cada propriedade listada é representada por um valor de um tipo específico. Por exemplo, o nome do elemento é uma string, enquanto a largura e a altura são inteiros. Todo o conjunto de valores descreve totalmente o objeto que deve aparecer no designer. O mais lógico é armazenar esse conjunto num local, para tal, foi criada uma classe especial, PropertySet. Mas que tipo de variáveis-membro ela deve conter?

À primeira vista, a solução óbvia parece ser usar variáveis de tipos internos simples. No entanto, eles carecem de um recurso importante que será necessário no futuro. MQL não tem suporte a referências a variáveis de tipo simples. E uma referência é uma coisa muito útil em algoritmos de processamento de IU. Aqui, geralmente pressupõe-se uma reação complexa à mudança de valores. Por exemplo, se for inserido um valor inválido num dos campos, vários "controles" dependentes serão bloqueados. Seria conveniente que esses "controles" pudessem gerenciar seu estado, guiados por um único local armazenando o valor a ser verificado. Isso pode ser feito mais facilmente com ajuda da "distribuição" de referências à mesma variável. Por isso, em vez de tipos integrados simples, usaremos uma classe wrapper genérica (convencionalmente chamada de Value) aproximadamente como mostrado a seguir.

  template<typename V>
  class Value
  {
    protected:
      V value;
      
    public:
      V operator~(void) const // getter
      {
        return value;
      }
      
      void operator=(V v)     // setter
      {
        value = v;
      }
  };

A palavra "aproximadamente" não foi escolhida ao acaso. Na verdade, à classe é adicionada mais uma funcionalidade, o que será discutido a seguir.

A presença de um wrapper de objeto permite interceptar a atribuição de novos valores no operador sobrecarregado '=', o que é impossível ao usar tipos simples. Nós precisamos dele.

Dada essa classe, o conjunto de propriedades do novo objeto de interface pode ser descrito mais ou menos assim.

  class PropertySet
  {
    public:
      Value<string> name;
      Value<int> type;
      Value<int> width;
      Value<int> height;
      Value<int> style; // VERTICAL_ALIGN / HORIZONTAL_ALIGN / ENUM_ALIGN_MODE
      Value<string> text;
      Value<color> clr;
      Value<int> align; // ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT
      Value<ushort> margins[4];
  };

Na caixa de diálogo do inspetor, teremos uma variável desta classe como um repositório centralizado das configurações atuais recebidas dos controles do inspetor.

Obviamente, o inspetor usa um controle adequado para definir cada uma das propriedades acima. Por exemplo, para selecionar o tipo de "controle" a ser criado é usada a lista suspensa CComboBox, já para o nome é implementado o campo de entrada CEdit. Uma propriedade é um valor único de determinado tipo (string, número, índice numa enumeração). Mesmo aquelas propriedades que são compostas - como o recuo - definidas separadamente para cada um dos 4 lados, devem ser consideradas de forma independente (esquerda, topo, etc.), uma vez que para elas serem inseridas serão reservados 4 campos de entrada e, portanto, cada valor será associado ao controle alocado a ele.

Assim, formularemos uma regra óbvia relativamente à caixa de diálogo do inspetor: nela, cada controle define uma propriedade associada a ela, que sempre tem um valor específico do tipo definido. Isso nos encaminha para próxima solução "arquitetônica".

Propriedades características de "controles"

Em artigos anteriores, apresentamos uma interface Notifiable especial que permitia definir o tratamento de eventos para um controle específico.

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

Aqui, C é uma das classes de "controle", por exemplo, CEdit, CSpinEdit, etc. O cache de layout chama o manipulador onEvent automaticamente para os elementos e tipos de eventos correspondentes. Obviamente, isso acontece apenas se inserirmos as strings corretas no mapa de tratamento de eventos. Por exemplo, na parte anterior, configuramos o processamento de cliques do botão Inject de acordo com este princípio (foi descrita como um herdeiro Notifiable<CButton>).

Nos casos em que um controle é usado para definir uma propriedade de um determinado tipo, ele exige que seja criada uma interface PlainTypeNotifiable mais especializada.

  template<typename C, typename V>
  class PlainTypeNotifiable: public Notifiable<C>
  {
    public:
      virtual V value() = 0;
  };

O propósito do método value é retornar desde um elemento do tipo C um valor do tipo V mais característico de C. Por exemplo, para a classe CEdit, é natural retornar um valor do tipo string (em alguma classe hipotética ExtendedEdit).

  class ExtendedEdit: public PlainTypeNotifiable<CEdit, string>
  {
    public:
      virtual string value() override
      {
        return Text();
      }
  };

Para cada tipo de "controle", há um único tipo de dados característico ou seu intervalo limitado (por exemplo, para inteiros, você pode especificar short, int, long). Todos os "controles" têm algum método getter pronto para facilitar um valor no método value substituído.

Assim, chegamos à essência da solução "arquitetônica", a interconexão das classes Value e PlainTypeNotifiable. Vamos implementá-la usando a classe-herdeira PlainTypeNotifiable, que coloca o valor do "controle" desde o inspetor na propriedade Value associada a ele.

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      Value<V> *property;
      
    public:
      void bind(Value<V> *prop)
      {
        property = prop;     // pointer assignment
        property = value();  // overloaded operator assignment for value of type V
      }
      
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CHANGE || event == ON_END_EDIT)
        {
          property = value();
          return true;
        }
        return false;
      };
  };

Graças à herança a partir da classe genérica PlainTypeNotifiable, a nova classe NotifiableProperty é tanto uma classe de "controle" C quanto um provedor de valores do tipo V.

Метод bind permite armazenar dentro do "controle" uma referência a Value e, em seguida, alterar o valor da propriedade do local (por referência), automaticamente quando o usuário interage com o "controle".

Por exemplo, para campos de entrada do tipo string, introduzimos a propriedade EditProperty, semelhante ao exemplo ExtendedEdit, mas herdada de NotifiableProperty:

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      virtual string value() override
      {
        return Text(); // Text() is a standard method of CEdit
      }
  };

Para uma lista suspensa, uma classe semelhante descreve uma propriedade com um valor inteiro.

  class ComboBoxProperty: public NotifiableProperty<ComboBoxResizable,int>
  {
    public:
      virtual int value() override
      {
        return (int)Value(); // Value() is a standard method of CComboBox
      }
  };

O programa descreve classes de propriedades-"controles" para todos os tipos básicos de elementos.

Diagrama de classes de "propriedades notificáveis"

Diagrama de classes de "propriedades notificáveis"

Agora é a hora de nos desembaraçar da palavra "aproximadamente" e nos familiarizar com as classes completas.

StdValue — valor, observação, dependências

Um pouco acima, já mencionamos uma situação comum em que é necessário monitorar a mudança de alguns "controles" para verificar a validade e alterar o estado de outros "controles". Em outras palavras, precisamos de algum tipo de observador que possa rastrear o estado de um "controle" e relatar suas alterações a outros "controles" interessados.

Para este propósito, introduzimos a interface StateMonitor (observador).

  class StateMonitor
  {
    public:
      virtual void notify(void *sender) = 0;
  };

O método notify deve ser chamado pela fonte de mudanças para que o observador em questão possa responder (se necessário). A origem das alterações pode ser identificada pelo parâmetro sender. Obviamente, a fonte das mudanças deve, de alguma forma, saber de antemão que um determinado observador está interessado em receber notificações. Para esses fins, a fonte deve implementar a interface Publisher.

  class Publisher
  {
    public:
      virtual void subscribe(StateMonitor *ptr) = 0;
      virtual void unsubscribe(StateMonitor *ptr) = 0;
  };

Com ajuda do método subscribe, o observador pode passar um referência a si mesmo para o "distribuidor". Como podemos imaginar, nossas fontes de mudanças serão propriedades e, portanto, a classe hipotética Value é, na verdade, herdada de Publisher e tem a seguinte formatação.

  template<typename V>
  class ValuePublisher: public Publisher
  {
    protected:
      V value;
      StateMonitor *dependencies[];
      
    public:
      V operator~(void) const
      {
        return value;
      }
      
      void operator=(V v)
      {
        value = v;
        for(int i = 0; i < ArraySize(dependencies); i++)
        {
          dependencies[i].notify(&this);
        }
      }
      
      virtual void subscribe(StateMonitor *ptr) override
      {
        const int n = ArraySize(dependencies);
        ArrayResize(dependencies, n + 1);
        dependencies[n] = ptr;
      }
      ...
  };

Qualquer observador registrado entra na matriz de dependencies; e quando o valor muda, ele será notificado com a chamada do seu método notify.

Como as propriedades são inequivocamente vinculadas aos "controles" com os quais foram introduzidas, forneceremos o armazenamento de uma referência ao "controle" na classe final de propriedades para a Biblioteca Padrão - StdValue (usa o tipo base de todos os "controles" CWnd).

  template<typename V>
  class StdValue: public ValuePublisher<V>
  {
    protected:
      CWnd *provider;
      
    public:
      void bind(CWnd *ptr)
      {
        provider = ptr;
      }
      
      CWnd *backlink() const
      {
        return provider;
      }
  };

Esta referência será útil mais tarde.

São as instâncias StdValue que preenchem PropertySet.

Diagrama de vínculos StdValue

Diagrama de vínculos StdValue

A classe NotifiableProperty acima também usa StdValue e, no método bind, vinculamos o valor-propriedade ao "controle" (this).

  template<typename C, typename V>
  class NotifiableProperty: public PlainTypeNotifiable<C,V>
  {
    protected:
      StdValue<V> *property;
    public:
      void bind(StdValue<V> *prop)
      {
        property = prop;
        property.bind(&this);        // +
        property = value();
      }
      ...
  };

Controle automático de estado dos "controles" — EnableStateMonitor

A forma mais comum de reagir às alterações em algumas configurações é bloquear ou desbloquear outros "controles" dependentes. O estado de cada "controle" adaptável pode depender de várias configurações (não necessariamente, de uma). Para monitorá-lo, desenvolvemos uma classe abstrata especial, EnableStateMonitorBase.

  template<typename C>
  class EnableStateMonitorBase: public StateMonitor
  {
    protected:
      Publisher *sources[];
      C *control;
      
    public:
      EnableStateMonitorBase(): control(NULL) {}
      
      virtual void attach(C *c)
      {
        control = c;
        for(int i = 0; i < ArraySize(sources); i++)
        {
          if(control)
          {
            sources[i].subscribe(&this);
          }
          else
          {
            sources[i].unsubscribe(&this);
          }
        }
      }
      
      virtual bool isEnabled(void) = 0;
  };

O "controle" cujo estado é "monitorado" por determinado observador é colocado no campo control. A matriz sources contém fontes de mudanças que afetam o estado. A matriz terá que ser preenchida nas classes-herdeiras. Quando anexamos um observador a um "controle" específico usando a chamada attach, o observador abarca todas as fontes de mudança. Em seguida, ele começa a receber prontamente notificações sobre mudanças nas fontes por meio de chamadas para seu método notify.

O método isEnabled decide se o "controle" deve ser bloqueado ou desbloqueado, aqui ele é declarado como abstrato e será implementado nas classes-herdeiras.

Para as classes da Biblioteca Padrão, há um mecanismo para bloquear "controles" com ajuda dos métodos comuns Enable e Disable. Nós os usaremos para implementar a classe concreta EnableStateMonitor.

  class EnableStateMonitor: public EnableStateMonitorBase<CWnd>
  {
    public:
      EnableStateMonitor() {}
      
      void notify(void *sender) override
      {
        if(control)
        {
          if(isEnabled())
          {
            control.Enable();
          }
          else
          {
            control.Disable();
          }
        }
      }
  };

Na prática, essa classe será usada no programa em muitos casos, mas consideraremos apenas um exemplo. Para criar novos objetos ou aplicar propriedades alteradas no designer, há um botão Apply na caixa de diálogo do inspetor (para ele é definida a classe ApplyButton, derivada de Notifiable<CButton>).

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          ...
        }
      };
  };

Ele deve ficar bloqueado se o nome do objeto não for especificado ou se seu tipo não for selecionado. Por isso, implementamos ApplyButtonStateMonitor com duas fontes de mudanças ("distribuidores"): nome e tipo.

  class ApplyButtonStateMonitor: public EnableStateMonitor
  {
    // what's required to detect Apply button state
    const int NAME;
    const int TYPE;
    
    public:
      ApplyButtonStateMonitor(StdValue<string> *n, StdValue<int> *t): NAME(0), TYPE(1)
      {
        ArrayResize(sources, 2);
        sources[NAME] = n;
        sources[TYPE] = t;
      }
      
      virtual bool isEnabled(void) override
      {
        StdValue<string> *name = sources[NAME];
        StdValue<int> *type = sources[TYPE];
        return StringLen(~name) > 0 && ~type != -1 && ~name != "Client";
      }
  };

O construtor da classe usa dois parâmetros que apontam para as propriedades correspondentes. Eles são armazenados na matriz sources. O método isEnabled verifica se o nome está preenchido e se o tipo está selecionado (não -1). Se as condições forem atendidas, o botão pode ser pressionado. Além disso, o nome é verificado em relação à string especial "Client", que é reservada nas caixas de diálogo da Biblioteca Padrão atrás da área do cliente e, portanto, não pode aparecer no nome de elementos personalizados.

A classe da caixa de diálogo do inspetor tem uma variável do tipo ApplyButtonStateMonitor, que é inicializada no construtor com referências a objetos StdValue que armazenam o nome e o tipo.

  class InspectorDialog: public AppDialogResizable
  {
    private:
      PropertySet props;
      ApplyButtonStateMonitor *applyMonitor;
    public:
      InspectorDialog::InspectorDialog(void)
      {
        ...
        applyMonitor = new ApplyButtonStateMonitor(&props.name, &props.type);
      }

No layout da caixa de diálogo, o nome e as propriedades do tipo são vinculados aos "controles" correspondentes, enquanto o observador, ao botão Apply.

          ...
          _layout<EditProperty> edit("NameEdit", BUTTON_WIDTH, BUTTON_HEIGHT, "");
          edit.attach(&props.name);
          ...
          _layout<ComboBoxProperty> combo("TypeCombo", BUTTON_WIDTH, BUTTON_HEIGHT);
          combo.attach(&props.type);
          ...
          _layout<ApplyButton> button1("Apply", BUTTON_WIDTH, BUTTON_HEIGHT);
          button1["enable"] <= false;
          applyMonitor.attach(button1.get());

Já conhecemos o método attach no objeto applyMonitor, mas o método attach nos objetos _layout é algo novo. Abordamos em detalhes a classe _layout no segundo artigo, já o método attach é a única mudança dessa versão. Este método-intermediário simplesmente chama bind para o controle gerado pelo objeto _layout dentro da caixa de diálogo do inspetor.

  template<typename T>
  class _layout: public StdLayoutBase
  {
      ...
      template<typename V>
      void attach(StdValue<V> *v)
      {
        ((T *)object).bind(v);
      }
      ...
  };

Lembre-se de que todas as propriedades-"controles" (incluindo EditProperty e ComboBoxProperty, como neste exemplo) são herdeiros da classe NotifiableProperty, que possui um método de vinculação para associar "controles" a variáveis StdValue que armazenam as propriedades correspondentes. Assim, os "controles" da janela do inspetor são associados às devidas propriedades que, por sua vez, são "monitoradas" pelo observador ApplyButtonStateMonitor. Assim que o usuário altera o valor de um dos dois campos, ele é exibido no PropertySet (lembre-se do manipulador onEvent para os eventos ON_CHANGE e ON_END_EDIT em NotifiableProperty) e notifica os observadores registrados, incluindo ApplyButtonStateMonitor. Como resultado, o estado do botão é alterado automaticamente para o atual.

A caixa de diálogo do inspetor exigirá vários monitores que gerenciem o estado dos "controles" de maneira semelhante. Descreveremos as regras de bloqueio específicas na seção manual do usuário.

Classes StateMonitor

Classes StateMonitor

Bem, vamos citar a que corresponde cada uma das propriedades do objeto criado e "controles" na caixa de diálogo do inspetor.

  • nome — EditProperty, string;
  • tipo — ComboBoxProperty, inteiro, número de tipo a partir da lista de itens suportados;
  • largura — SpinEditPropertySize, inteiro, pixels;
  • altura — SpinEditPropertySize, inteiro, pixels;
  • estilo — ComboBoxProperty, inteiro igual ao valor de uma das enumerações (dependendo do tipo de elemento): VERTICAL_ALIGN (CBoxV), HORIZONTAL_ALIGN (CBoxH), ENUM_ALIGN_MODE (CEdit);
  • texto — EditProperty, string;
  • cor de fundo — ComboBoxColorProperty, valor da cor a partir da lista;
  • alinhamento de borda — AlignCheckGroupProperty, bitmask, grupo de sinalizadores independentes (ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT);
  • recuos — quatro SpinEditPropertyShort, inteiros;

O nome das classes de algumas "Property" indica sua particularidade, ou seja, a expansão da funcionalidade em comparação com a implementação básica, que as "simples" SpinEditProperty, ComboBoxProperty, CheckGroupProperty, etc. oferecem. O manual do usuário explica em detalhes para que servem.

Para representar esses "controles" de maneira organizada e visual, o layout da caixa de diálogo inclui contêineres adicionais e rótulos com informações. O código completo pode ser encontrado no anexo.

Manipulação de eventos

O tratamento de eventos para todos os "controles" é definido no mapa de eventos:

  EVENT_MAP_BEGIN(InspectorDialog)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_END_EDIT, cache, EditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, SpinEditProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, SpinEditPropertyShort)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, ComboBoxColorProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CHANGE, cache, AlignCheckGroupProperty)
    ON_EVENT_LAYOUT_CTRL_DLG(ON_CLICK, cache, ApplyButton)
    ...
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache) // default (stub)
  EVENT_MAP_END(AppDialogResizable)

Para melhorar a eficiência do tratamento de eventos no cache, foram implementadas algumas medidas especiais. As macros ON_EVENT_LAYOUT_CTRL_ANY e ON_EVENT_LAYOUT_CTRL_DLG, apresentadas no segundo artigo, baseiam seu trabalho em encontrar um "controle" na matriz de cache por um número único recebido desde o sistema no parâmetro lparam. Ao fazer isso, a implementação subjacente do cache realiza uma pesquisa linear na matriz.

Para acelerar o processo, o método buildIndex foi adicionado à classe MyStdLayoutCache (herdada de StdLayoutCache), cuja instância é armazenada e usada no inspetor. Sua conveniente capacidade de indexação depende do recurso da Biblioteca Padrão para atribuir números exclusivos a todos os elementos. No método CAppDialog::Run, um número aleatório é selecionado - o já conhecido m_instance_id - para iniciar a numeração de todos os objetos do gráfico criados pela janela. Assim, podemos saber a amplitude dos valores obtidos. Menos o m_instance_id, cada valor lparam que vem com o evento se torna um número de objeto direto. No entanto, o programa cria muitos mais objetos no gráfico do que os armazenados no cache, porque muitos "controles" (e a própria janela, como o conjunto de bordas, título, botões de minimização, etc.) consistem em muitos objetos de baixo nível. Portanto, o índice no cache nunca corresponde ao ID do objeto menos m_instance_id. A este respeito, foi necessário alocar uma matriz de índice especial (seu tamanho é igual ao número de objetos da janela), enquanto para aqueles "controles" "reais" que estão disponíveis para o cache, foi necessário, de alguma forma, escrever seus números de sequência no cache. Como resultado, o acesso é fornecido quase instantaneamente, usando o princípio de endereçamento indireto.

Só precisamos preencher a matriz após a implementação de CAppDialog::Run atribuir números exclusivos, mas antes de o manipulador OnInit concluir seu trabalho. É melhor tornar o método Run virtual para esses fins (ele não está na Biblioteca padrão) e substituí-lo no InspectorDialog, por exemplo, desta forma.

  bool InspectorDialog::Run(void)
  {
    bool result = AppDialogResizable::Run();
    if(result)
    {
      cache.buildIndex();
    }
    return result;
  }

O método buildIndex em si é bastante simples.

  class MyStdLayoutCache: public StdLayoutCache
  {
    protected:
      InspectorDialog *parent;
      // fast access
      int index[];
      int start;
      
    public:
      MyStdLayoutCache(InspectorDialog *owner): parent(owner) {}
      
      void buildIndex()
      {
        start = parent.GetInstanceId();
        int stop = 0;
        for(int i = 0; i < cacheSize(); i++)
        {
          int id = (int)get(i).Id();
          if(id > stop) stop = id;
        }
        
        ArrayResize(index, stop - start + 1);
        ArrayInitialize(index, -1);
        for(int i = 0; i < cacheSize(); i++)
        {
          CWnd *wnd = get(i);
          index[(int)(wnd.Id() - start)] = i;
        }
      ...
  };

Agora podemos escrever uma implementação rápida do método para localizar "controles" por número.

      virtual CWnd *get(const long m) override
      {
        if(m < 0 && ArraySize(index) > 0)
        {
          int offset = (int)(-m - start);
          if(offset >= 0 && offset < ArraySize(index))
          {
            return StdLayoutCache::get(index[offset]);
          }
        }
        
        return StdLayoutCache::get(m);
      }

Mas chega de falar sobre a estrutura interna do inspetor.

Esta é a aparência de sua janela num programa em execução.

Caixa de diálogo Inspector e formulário Designer

Caixa de diálogo Inspector e formulário Designer

Além das propriedades, vemos alguns elementos desconhecidos aqui. Todos eles serão descritos mais tarde. Vamos prestar atenção ao botão Apply por enquanto. Após o usuário definir os valores das propriedades, ele pode gerar o objeto solicitado no formulário do designer clicando neste botão. Tendo uma classe que deriva de Notifiable, o botão é capaz de manipular cliques em seu próprio método onEvent.

  class ApplyButton: public Notifiable<CButton>
  {
    public:
      virtual bool onEvent(const int event, void *parent) override
      {
        if(event == ON_CLICK)
        {
          Properties p = inspector.getProperties().flatten();
          designer.inject(p);
          ChartRedraw();
          return true;
        }
        return false;
      };
  };

Lembre-se de que o inspetor de variáveis e o designer são objetos globais com uma caixa de diálogo do inspetor e um formulário do designer, respectivamente. O inspetor tem um método getProperties em sua API para expor o conjunto atual de propriedades PropertySet descritas acima:

    PropertySet *getProperties(void) const
    {
      return (PropertySet *)&props;
    }

PropertySet pode se envolver numa estrutura Properties plana (regular) para passar para o método inject do designer. Então fazemos uma transição suave para a janela do designer.

Designer

Se descartarmos as verificações auxiliares, a essência do método inject é semelhante ao que vimos no final do segundo artigo, isto é, o formulário empurra o contêiner de destino para a pilha de layout (no segundo artigo ele foi definido estaticamente, ou seja, era sempre o mesmo) e gera um elemento nele com as propriedades passadas. No novo formulário, todos os elementos podem ser selecionados com um clique do mouse e, assim, alterar o contexto de inserção. Além disso, um clique inicia a transferência das propriedades do item selecionado para o inspetor. Assim, torna-se possível editar as propriedades dos objetos já criados e atualizá-los através do mesmo botão Apply. O designer determina se o usuário deseja introduzir um novo elemento ou editar um antigo, comparando o nome e o tipo do elemento. Se essa combinação já estiver no cache do designer, estaremos falando sobre edição.

Em termos gerais, é assim que se vê a adição de um novo elemento.

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        ...
      }
      else
      {
        CBox *box = dynamic_cast<CBox *>(cache.getSelected());
        
        if(box == NULL) box = cache.findParent(cache.getSelected());
        
        if(box)
        {
          CWnd *added;
          StdLayoutBase::setCache(cache);
          {
            _layout<CBox> injectionPanel(box, box.Name());
            
            {
              AutoPtr<StdLayoutBase> base(getPtr(props));
              added = (~base).get();
              added.Id(rand() + ((long)rand() << 32));
            }
          }
          box.Pack();
          cache.select(added);
        }
      }

A variável cache é descrita em DesignerForm e contém um objeto da classe DefaultStdLayoutCache, que deriva de StdLayoutCache (apresentado em artigos anteriores). StdLayoutCache permite encontrar um objeto por nome usando o método get. Caso contrário, estaremos falando de um novo objeto e o designer tentará determinar o contêiner selecionado pelo usuário atual. Para fazer isso, o método getSelected é implementado na nova classe DefaultStdLayoutCache. Veremos exatamente como a seleção funciona um pouco mais tarde. É importante notar aqui que apenas um contêiner pode ser o local de introdução de um novo elemento (no nosso caso, são utilizados contêineres da família CBox). Se nenhum contêiner estiver selecionado, o algoritmo chamará findParent para determinar o contêiner pai e usá-lo como destino. Quando determinado o ponto de inserção, o habitual esquema de marcação com blocos aninhados começa a correr. No bloco externo, é criado um objeto _layout com o contêiner de destino e, em seguida, o objeto é gerado internamente, na linha:

  AutoPtr<StdLayoutBase> base(getPtr(props));

Todas as propriedades são passadas para o método auxiliar getPtr. Ele sabe como criar objetos de todos os tipos suportados, mas por uma questão de simplicidade, mostraremos como funciona apenas alguns deles.

    StdLayoutBase *getPtr(const Properties &props)
    {
      switch(props.type)
      {
        case _BoxH:
          {
            _layout<CBoxH> *temp = applyProperties(new _layout<CBoxH>(props.name, props.width, props.height), props);
            temp <= (HORIZONTAL_ALIGN)props.style;
            return temp;
          }
        case _Button:
          return applyProperties(new _layout<CButton>(props.name, props.width, props.height), props);
        case _Edit:
          {
            _layout<CEdit> *temp = applyProperties(new _layout<CEdit>(props.name, props.width, props.height), props);
            temp <= (ENUM_ALIGN_MODE)LayoutConverters::style2textAlign(props.style);
            return temp;
          }
        case _SpinEdit:
          {
            _layout<SpinEditResizable> *temp = applyProperties(new _layout<SpinEditResizable>(props.name, props.width, props.height), props);
            temp["min"] <= 0;
            temp["max"] <= DUMMY_ITEM_NUMBER;
            temp["value"] <= 1 <= 0;
            return temp;
          }
        ...
      }
    }

Objetos _Layout, modelados por um determinado tipo de elemento GUI, são criados usando construtores habituais para nós graças às descrições estáticas de marcação MQL. Objetos _Layout permitem que usemos operadores sobrecarregados <= para definir propriedades, assim preenchemos o estilo HORIZONTAL_ALIGN de um CBoxH, ENUM_ALIGN_MODE para uma caixa de texto ou intervalos 'spinners'. Algumas outras configurações de propriedades comuns (preenchimento, texto, cor) são delegadas ao método auxiliar applyProperties (consulte o código-fonte para obter detalhes).

    template<typename T>
    T *applyProperties(T *ptr, const Properties &props)
    {
      static const string sides[4] = {"left", "top", "right", "bottom"};
      for(int i = 0; i < 4; i++)
      {
        ptr[sides[i]] <= (int)props.margins[i];
      }
      
      if(StringLen(props.text))
      {
        ptr <= props.text;
      }
      else
      {
        ptr <= props.name;
      }
      ...
      return ptr;
    }

Se o objeto for encontrado no cache por nome, acontece o seguinte (de forma simplificada):

    void inject(Properties &props)
    {
      CWnd *ptr = cache.get(props.name);
      if(ptr != NULL)
      {
        CWnd *sel = cache.getSelected();
        if(ptr == sel)
        {
          update(ptr, props);
          Rebound(Rect());
        }
      }
      ...
    }

O método update auxiliar transfere propriedades da estrutura props para o objeto ptr encontrado.

    void update(CWnd *ptr, const Properties &props)
    {
      ptr.Width(props.width);
      ptr.Height(props.height);
      ptr.Alignment(convert(props.align));
      ptr.Margins(props.margins[0], props.margins[1], props.margins[2], props.margins[3]);
      CWndObj *obj = dynamic_cast<CWndObj *>(ptr);
      if(obj)
      {
        obj.Text(props.text);
      }
      
      CBoxH *boxh = dynamic_cast<CBoxH *>(ptr);
      if(boxh)
      {
        boxh.HorizontalAlign((HORIZONTAL_ALIGN)props.style);
        boxh.Pack();
        return;
      }
      CBoxV *boxv = dynamic_cast<CBoxV *>(ptr);
      if(boxv)
      {
        boxv.VerticalAlign((VERTICAL_ALIGN)props.style);
        boxv.Pack();
        return;
      }
      CEdit *edit = dynamic_cast<CEdit *>(ptr);
      if(edit)
      {
        edit.TextAlign(LayoutConverters::style2textAlign(props.style));
        return;
      }
    }

Agora, vamos voltar ao destaque dos elementos da GUI no formulário. Sua solução é controlada pelo objeto de cache, graças ao tratamento de eventos iniciados pelo usuário. O manipulador onEvent é reservado na classe StdLayoutCache, que se conecta aos eventos do gráfico no mapa usando a macro ON_EVENT_LAYOUT_ARRAY:

  EVENT_MAP_BEGIN(DesignerForm)
    ON_EVENT_LAYOUT_ARRAY(ON_CLICK, cache)
    ...
  EVENT_MAP_END(AppDialogResizable)

Isso envia os cliques do mouse de todos os itens de cache para o manipulador onEvent que definimos em nossa classe derivada DefaultStdLayoutCache. Na classe é criado ponteiro selected do tipo de janela genérico CWnd e deve ser preenchido pelo manipulador onEvent.

  class DefaultStdLayoutCache: public StdLayoutCache
  {
    protected:
      CWnd *selected;
      
    public:
      CWnd *getSelected(void) const
      {
        return selected;
      }
      
      ...
      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(element); // get actual GUI element
          }
          ...
          
          selected = element;
          const bool b = highlight(selected, clrRed);
          Print(control.Name(), " -> ", element._rtti, " / ", element.Name(), " / ", element.Id());
          EventChartCustom(CONTROLS_SELF_MESSAGE, ON_LAYOUT_SELECTION, 0, 0.0, NULL);
          return true;
        }
        return false;
      }
  };

O realce visual de um elemento num formulário é feito com uma borda vermelha no método trivial highlight (chamando ColorBorder). O manipulador primeiro desmarca o item selecionado anteriormente (define a cor da borda para CONTROLS_BUTTON_COLOR_BORDER), em seguida, encontra o item de cache correspondente ao objeto gráfico clicado e armazena o ponteiro para ele na variável selecionada. No final, o novo objeto selecionado é marcado com uma borda vermelha, enquanto o evento ON_LAYOUT_SELECTION é enviado para o gráfico. Ele permite que o inspetor saiba que um novo elemento foi selecionado no formulário e, portanto, é preciso mostrar suas propriedades na caixa de diálogo do inspetor.

No inspetor, esse evento é interceptado no manipulador OnRemoteSelection, que solicita uma referência ao objeto selecionado do designer e lê todos os atributos dele por meio da API da biblioteca padrão.

  EVENT_MAP_BEGIN(InspectorDialog)
    ...
    ON_NO_ID_EVENT(ON_LAYOUT_SELECTION, OnRemoteSelection)
  EVENT_MAP_END(AppDialogResizable)

Aqui está o início do método OnRemoteSelection.

  bool InspectorDialog::OnRemoteSelection()
  {
    DefaultStdLayoutCache *remote = designer.getCache();
    CWnd *ptr = remote.getSelected();
    
    if(ptr)
    {
      string purename = StringSubstr(ptr.Name(), 5); // cut instance id prefix
      CWndObj *x = dynamic_cast<CWndObj *>(props.name.backlink());
      if(x) x.Text(purename);
      props.name = purename;
      
      int t = -1;
      ComboBoxResizable *types = dynamic_cast<ComboBoxResizable *>(props.type.backlink());
      if(types)
      {
        t = GetTypeByRTTI(ptr._rtti);
        types.Select(t);
        props.type = t;
      }
      
      // width and height
      SpinEditResizable *w = dynamic_cast<SpinEditResizable *>(props.width.backlink());
      w.Value(ptr.Width());
      props.width = ptr.Width();
      
      SpinEditResizable *h = dynamic_cast<SpinEditResizable *>(props.height.backlink());
      h.Value(ptr.Height());
      props.height = ptr.Height();
      ...
    }
  }

Após recebida uma referência ptr ao objeto selecionado do cache do designer, o algoritmo descobre seu nome, limpa-o do identificador de janela (este campo m_instance_id na classe CAppDialog é um prefixo em todos os nomes para que não haja conflitos entre objetos de janelas diferentes, e temos 2 deles), e grava no "controle" associado ao nome. Observe que é aqui que usamos a referência inversa ao "controle" (backlink()) da propriedade StdValue<string> name. Além disso, como estamos modificando o campo por dentro, o evento sobre sua mudança não é gerado (como acontece quando o usuário inicia a mudança) e, portanto, é necessário escrever um novo valor no PropertySet correspondente (props.name).

Teoricamente, do ponto de vista OOP, seria mais correto para cada tipo de propriedade de "controle" substituir seu método de mudança virtual e atualizar a instância StdValue vinculada a ele automaticamente. Por exemplo, vejamos como isso poderia ser feito para CEdit.

  class EditProperty: public NotifiableProperty<CEdit,string>
  {
    public:
      ...
      virtual bool OnSetText(void) override
      {
        if(CEdit::OnSetText())
        {
          if(CheckPointer(property) != POINTER_INVALID) property = m_text;
          return true;
        }
        return false;
      }    
  };

Em seguida, alterar o conteúdo do campo usando o método Text() resultaria numa chamada subsequente para OnSetText e uma atualização automática de property. Mas não é tão conveniente fazer isso para controles compostos como CCheckGroup, portanto, é preferida uma implementação mais utilitária.

Da mesma forma, utilizando referências inversas aos "controles", atualizamos o conteúdo nos campos de altura, largura, tipo e outras propriedades do objeto selecionado no designer.

Para identificar os tipos suportados, temos uma enumeração, cujo elemento pode ser determinado com base na variável especial _rtti, que adicionamos no nível mais baixo nos artigos anteriores à classe CWnd, e a preenchemos em todas as classes derivadas com o nome de uma classe particular.

Manual do usuário

A caixa de diálogo do inspetor contém campos de entrada de vários tipos com propriedades do objeto atual (selecionado no designer) ou do objeto que deve ser criado.

O nome (string) e o tipo (selecionado na lista suspensa) são obrigatórios.

Os campos de largura e altura permitem definir o tamanho do objeto em pixels. No entanto, essas configurações não são levadas em consideração se um modo de ampliação específico for especificado abaixo: por exemplo, o fato de encaixar nas bordas esquerda e direita indica a largura com as dimensões do contêiner. Ao clicar no campo de altura ou largura enquanto mantemos pressionada a tecla Shift, podemos redefinir a propriedade para seu valor padrão (largura - 100, altura - 20).

Todos os "controles" do tipo SpinEdit (não apenas nas propriedades de tamanho) foram aprimorados de forma que mover o mouse dentro do "controle" para a esquerda ou para a direita enquanto se mantém pressionado o botão do mouse (arraste, mas não solte) mude rapidamente os valores do spinner em proporção à distância percorrida em píxeis. Isso é feito para facilitar a edição, o que não é muito conveniente ao carregar em pequenos botões basculantes. As alterações estão disponíveis para quaisquer programas que usem "controles" da pasta ControlsPlus.

A lista suspensa com o estilo de alinhamento de conteúdo (Estilo) está disponível apenas para elementos CBoxV, CBoxH e CEdit (está bloqueada para outros tipos). Para contêineres CBox são usados todos os modos de alinhamento ("center", "justify", "left/top", "right/bottom", "stack"). Para CEdit, funcionam apenas aqueles que correspondem a ENUM_ALIGN_MODE ("center", "left", "right").

O campo Text permite definir o título de botão CButton, a margem CLabel ou o conteúdo CEdit. Para outros tipos, o campo está inativo.

A lista suspensa Color foi projetada para selecionar uma cor de fundo na lista de cores da web. Ele está disponível apenas para CBoxH, CBoxV, CButton e CEdit. Outros tipos de "controles" como são compostos requerem uma técnica mais sofisticada de atualização de cores em todos os seus componentes, por isso decidiu-se por enquanto não suportar sua colorização. A classe CListView foi modificada para selecionar cores. Nela foi adicionado um modo especial de "cor", no qual os valores dos itens da lista são tratados como códigos de cores, e o fundo de cada item é desenhado com a cor correspondente. Este modo é habilitado pelo método SetColorMode, e é utilizado na nova classe ComboBoxWebColors (especialização ComboBoxResizable da pasta Layouts).

As cores padrão da biblioteca GUI não podem ser selecionadas no momento, porque há um problema com a definição das cores padrão. É importante sabermos a cor padrão de cada tipo de "controle" para não mostrá-la selecionado na lista quando o usuário não seleciona nenhuma cor especial. A abordagem mais simples é criar um "controle" vazio de um tipo específico e ler a propriedade ColorBackground nele, mas funciona para um número muito limitado de "controles". Acontece que a cor, via de regra, não é atribuída no construtor da classe, mas, sim, no método Create, o que acarreta muitas inicializações desnecessárias, incluindo a criação de objetos reais no gráfico. Claro, não precisamos de objetos desnecessários. Além disso, a cor de fundo de muitos objetos compostos é derivada do fundo do substrato, em vez de do "controle" principal. Devido à dificuldade em levar em consideração essas nuances, optou-se por considerar como não selecionadas todas as cores utilizadas por padrão em qualquer classe de "controles" da Biblioteca Padrão. Isso significa que eles não podem ser incluídos na lista, caso contrário, o usuário pode escolher essa cor, mas como resultado, ele não verá a confirmação de sua escolha no inspetor. Listas de cores da web e cores GUI padrão são fornecidas no arquivo LayoutColors.mqh.

Para redefinir a cor para o valor padrão (diferente para cada tipo de "controle"), selecionamos a primeira opção "vazia" da lista correspondente a clrNONE.

Os sinalizadores do grupo de alternadores independentes Alignment correspondem aos modos de alinhamento lateral da enumeração ENUM_WND_ALIGN_FLAGS, mais um modo WND_ALIGN_CONTENT especial, descrito no segundo artigo, que funciona apenas para contêineres. Se segurarmos a tecla Shift enquanto pressionarmos qualquer tecla de alternância, o programa irá alternar sincronizadamente todos os 4 sinalizadores ENUM_WND_ALIGN_FLAGS. Se a opção for habilitada, as outras também serão habilitadas, e vice-versa, se a opção for desabilitada, as outras serão zeradas. Isso permite alternar todo o grupo com um clique, exceto para WND_ALIGN_CONTENT.

Os "spinners" Margins definem o preenchimento de um elemento em relação aos lados do retângulo do contêiner em que esse elemento está localizado. Ordem dos campos: esquerda, superior, direita, inferior. Podemos redefinir rapidamente todos os campos para zero clicando em qualquer campo enquanto mantemos pressionada a tecla Shift. Todos os campos podem ser facilmente configurados como iguais clicando no campo com o valor necessário enquanto se mantém pressionada a tecla Ctrl, como resultado, o valor será copiado para os outros 3 campos.

Já estamos familiarizados com o botão Apply, ele aplica as alterações feitas, como resultado das quais um novo "controle" é criado no designer ou o antigo é modificado.

Um novo objeto é inserido no objeto-contêiner selecionado ou no contêiner que contém o "controle" selecionado (se o "controle" estiver selecionado).

Para selecionar um elemento no designer, é necessário clicar nele com o mouse. O item selecionado é destacado com uma borda vermelha. A exceção é CLabel, uma vez que ele não oferece suporte a esse recurso.

O novo elemento é selecionado automaticamente imediatamente após a inserção.

Numa caixa de diálogo vazia só pode ser inserido um contêiner CBoxV ou CBoxH, e não é necessário selecionar a área do cliente com antecedência. Este primeiro e maior contêiner se estende para preencher a janela inteira por padrão.

Clicar novamente num item já selecionado aciona uma solicitação de exclusão. A remoção ocorre somente após a confirmação do usuário.

O botão de duas posições TestMode alterna entre os dois modos do designer. Por padrão, está liberado, o modo de teste é desabilitado, enquanto a edição da interface do designer funciona, assim, o usuário pode selecionar elementos com cliques do mouse e excluí-los. Quando pressionado, é ativado o modo de teste. Nesse caso, a caixa de diálogo funciona aproximadamente como num programa real, a edição do layout e a seleção de elementos são desativadas.

O botão Export permite salvar a configuração atual da interface do designer como um layout MQL. O nome do arquivo começa com o prefixo layout, contém o instantâneo da hora atual e a extensão txt. Se segurarmos o botão Shift enquanto pressionarmos Export, a configuração do formulário será salva não em texto, mas, sim, em formato binário, num arquivo de formato próprio com extensão mql. Isso é conveniente porque podemos interromper o processo de design da marcação a qualquer momento e retomá-lo depois de um tempo. Para carregar um arquivo mql binário do layout, é usado o mesmo botão Export, desde que o formulário e o cache de elementos estejam vazios, o qual é executado imediatamente após o início do programa. A versão atual sempre tenta importar o arquivo "layout.mql". Quem desejar pode implementar escolha de arquivo nos parâmetros de entrada ou em MQL.

Na parte superior da caixa de diálogo do inspetor, há uma lista suspensa com todos os elementos criados no designer. Ao selecionar um item na lista, ele é selecionado e destacado no designer automaticamente. Por outro lado, ao destacar um item no formulário, ele se torna atual na lista.

Agora, durante o processo de edição, podem ocorrer erros de 2 categorias: aqueles que podem ser corrigidos pela análise da marcação MQL, e outros mais graves. Os primeiros incluem tais combinações de configurações quando "controles" ou contêineres vão além da janela ou contêiner pai. Nesse caso, eles, via de regra, deixam de ser selecionados pelo mouse e só podem ser ativados usando o seletor no inspetor. Só uma análise da marcação MQL na forma de texto pode estabelecer quais propriedades estão erradas, para obter seu estado atual, basta clicar no botão Export. Depois de estudar a marcação, devemos corrigir as propriedades no inspetor e, assim, restaurar a forma correta do formulário.

Esta versão do programa se destina à verificação da ideia por trás do artigo, e o código-fonte não contém verificações para todas as combinações de parâmetros que podem surgir ao redimensionar contêineres adaptativos.

A segunda categoria de erros inclui, em particular, aqueles que ocorrem ao inserir um elemento no contêiner errado por engano. Nesse caso, só podemos excluir o elemento e adicioná-lo novamente, mas, num local diferente.

É recomendável salvar periodicamente o formulário em formato binário (botão Export com a tecla Shift pressionada) para que em caso de problemas insolúveis possamos continuar trabalhando com a última configuração válida.

Vejamos alguns exemplos de como trabalhar com o programa.

Exemplos

Primeiro, tentemos reproduzir a estrutura do inspetor no designer. A animação a seguir mostra o início do processo adicionando as quatro primeiras linhas e campos para definir o nome, tipo e largura. Usamos vários tipos de "controles", alinhamentos, cores. Geramos o rótulos com nomes de campo usando campos de entrada CEdit, porque CLabel tem funcionalidade muito limitada (em particular, não suporta alinhamento de texto e cor de fundo). Porém, como no inspetor não há configuração do atributo 'somente leitura', a única maneira de definir o rótulo como não editável é atribuir um fundo cinza (isso é puramente um efeito visual). No código MQL, esses objetos CEdit, obviamente, devem ser adicionalmente configurados conforme necessário, ou seja, eles devem ser alternados para o modo 'somente leitura'. Isso é exatamente o que é feito no próprio inspetor.

Processo de edição de formulário

Processo de edição de formulário

A edição do formulário mostra claramente a natureza adaptativa da abordagem de marcação e como o visual é, de maneira única, vinculado pela marcação MQL. A qualquer momento, podemos clicar no botão Export e ver o código MQL resultante.

Na versão final, podemos obter um diálogo que corresponde quase completamente à janela do inspetor (com exceção de alguns traços).

Marcação da caixa de diálogo do Inspetor recriada no designer

Marcação da caixa de diálogo do Inspetor recriada no designer

No entanto, deve-se lembrar que dentro do inspetor, muitas classes de "controles" não são padronizadas, pois são herdadas de uma ou outra x-Property e fornecem vinculação algorítmica adicional. Em nosso exemplo, no designer usamos apenas as classes de "controle" padrão (ControlsPlus). Em outras palavras, o layout resultante sempre contém apenas o visual do programa e o comportamento padrão dos "controles". É apenas o programador que tem privilégio de rastrear o estado dos elementos e codificar uma reação à sua mudança, incluindo a possível customização das classes. O sistema criado permite que alterar entidades na marcação MQL como na MQL comum. Ou seja, podemos substituir, por exemplo, ComboBox por ComboBoxWebColors. Mas, em qualquer caso, todas as classes mencionadas no layout devem ser anexadas ao projeto usando as diretivas #include.

Salvamos a caixa de diálogo acima (inspetor duplicado) usando o comando Export para arquivos binários e de texto - ambos estão anexados ao artigo com os nomes layout-inspector.txt e layout-inspector.mql, respectivamente.

Ao examinar o arquivo de texto, podemos entender a essência da marcação do inspetor sem estarmos preso a algoritmos e dados.

Basicamente, após exportar a marcação para um arquivo, seu conteúdo pode ser inserido em qualquer projeto ao qual estejam vinculados os arquivos de cabeçalho do sistema de layout e todas as classes GUI utilizadas. O resultado será um trecho de interface funcional. Em particular, ao artigo é anexado um projeto com uma caixa de diálogo DummyForm vazia. Os interessados podem encontrar nele o método CreateLayout e inserir a marcação MQL, que será preparada com antecedência no designer.

Isso também é fácil de fazer para layout-inspector.txt. Copie todo o conteúdo deste arquivo para a área de transferência e cole-o no arquivo DummyForm.mqh dentro do método CreateLayout, no comentário "// insert exported MQL-layout here".

Observe que na representação textual do layout há uma menção ao tamanho da caixa de diálogo (neste caso 200*350) com o qual foi criada. Por isso, no código-fonte de CreateLayout, após a linha de criação de objeto com formulário _layout <DummyForm> dialog (this...) e antes do layout copiado devem ser inseridas as linhas:

  Width(200);
  Height(350);
  CSize sz = {200, 350};
  SetSizeLimit(sz);

Isso dará espaço suficiente para todos os "controles" e não tornará a caixa de diálogo mais pequena.

Não geramos o fragmento correspondente automaticamente ao exportar, pois o layout pode representar apenas uma parte da caixa de diálogo ou, no futuro, servir a outras classes de janelas e contêineres, onde esses métodos não estarão presentes.

Se agora compilarmos e iniciarmos o exemplo, obteremos uma cópia muito semelhante do inspetor. Porém, exitem diferenças.

Interface do inspetor reconstruída

Interface do inspetor reconstruída

Em primeiro lugar, todas as listas suspensas estão vazias e, portanto, não funcionam. Todos os spinners não estão configurados e também não funcionam. O grupo de sinalizadores de alinhamento está visualmente vazio, porque não geramos nenhuma "caixa de seleção" no layout, mas o "controle" correspondente existe e ainda contém 5 "caixas de seleção" ocultas que a biblioteca de componentes padrão gera com base no tamanho inicial do "controle" (todos esses objetos podem ser vistos na lista de objetos do gráfico, comando Object List).

Em segundo lugar, o grupo de spinners com valores de indentação está ausente, não o transferimos para o formulário, porque no inspetor ele é criado por um objeto de layout como matriz. Nosso editor não pode fazer isso. Seria possível criar 4 elementos independentes, mas então todos eles teriam que ser configurados no código de forma semelhante entre si.

Ao clicar em qualquer "controle", o formulário exibe seu nome, classe e identificador no log.

Também podemos carregar o arquivo binário layout-inspector.mql (primeiro, renomeado para layout.mql) de volta para o inspetor e continuar a editá-lo. Para fazer isso, basta iniciar o projeto principal e clicar em Export enquanto o formulário ainda está vazio.

Observe que o designer gera, para maior clareza, uma certa quantidade de dados para todos os "controles" com listas ou grupos, e também define o intervalo para os spinners. Por isso, ao alternar para TestMode, podemos "brincar" com os elementos. Este tamanho de pseudo-dados é definido no formulário do designer pela macro DUMMY_ITEM_NUMBER e é igual a 11 por padrão.

Agora vamos ver como o painel de negociação pode ficar no designer.

Layout do painel de negociação Color-Cube-Trade-Panel

Layout do painel de negociação Color-Cube-Trade-Panel

Ele não pretende ser súper-funcional, mas a questão é que podemos alterá-lo facilmente de acordo com as preferências de um determinado trader. Este formulário, como o anterior, usa contêineres multicoloridos para facilitar a compreensão de sua localização.

Mais uma vez, manifestaremos nossa reserva de que estamos falando apenas do visual. Na saída do designer, obtemos um código MQL responsável apenas por gerar a janela e o estado inicial dos "controles". Todos os algoritmos de cálculo, reação às ações do usuário, proteção contra entrada incorreta de dados e envio de ordens de negociação, como de costume, devem ser programados manualmente.

Neste layout, temos que mudar alguns tipos de "controles" para algo mais adequado. Portanto, as datas de vencimento de ordens pendentes são indicadas segundo "Calendário", mas não suporta entrada de tempo. Todas as listas suspensas devem ser preenchidas com as opções apropriadas, por exemplo, os níveis de stop podem ser inseridos em unidades diferentes (preço, distância em pontos, risco (perda) como uma porcentagem do depósito ou em valor absoluto), o volume pode ser definido como fixo, em dinheiro ou como uma porcentagem da margem livre, etc., já o trailing é um dos vários algoritmos.

Esta marcação é anexada ao artigo na forma de dois arquivos layout-color-cube-trade-panel: texto e binário. O primeiro pode ser inserido num formulário vazio (como DummyForm) e complementado com dados e manipulação de eventos. O segundo deve ser carregado no designer para editá-lo. Mas não se esqueça de que o editor gráfico é opcional. Você também pode corrigir a marcação durante a visualização de texto. A única vantagem do editor é que podemos brincar com as configurações e ver as alterações instantaneamente. No entanto, ele suporta apenas as propriedades mais básicas.

Fim do artigo

Neste artigo, examinamos um editor simples para o desenvolvimento interativo de uma interface gráfica para programas construídos na abordagem de marcação MQL. A implementação apresentada inclui apenas recursos básicos, mas ao mesmo tempo suficiente para mostrar o desempenho da ideia apresentada hoje, ampliar outros tipos de "controles" no futuro, fornecer um suporte mais completo das diferentes propriedades, outras bibliotecas de componentes GUI e mecanismos de edição. Em particular, o editor ainda não tem a função de cancelar operações, inserir elementos numa posição arbitrária no contêiner (ou seja, não apenas adicionar ao final da lista de "controles" existentes), operações de grupo, copiar e colar da área de transferência e muito mais. No entanto, o código aberto permite que você complemente e adapte a tecnologia às suas próprias necessidades.

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

Arquivos anexados |
MQL5GUI3.zip (112.66 KB)
Trabalhando com séries temporais na biblioteca DoEasy (Parte 42): classe de um objeto de buffer abstrato de indicador Trabalhando com séries temporais na biblioteca DoEasy (Parte 42): classe de um objeto de buffer abstrato de indicador
Com este artigo começaremos a criar classes de buffers de indicador para a biblioteca DoEasy. Hoje, criaremos uma classe base de buffer abstrato que será o alicerce para a criação de diversos tipos de classes de buffer de indicador.
Trabalhando com séries temporais na biblioteca DoEasy (Parte 41): exemplo de indicador multissímbolo multiperíodo Trabalhando com séries temporais na biblioteca DoEasy (Parte 41): exemplo de indicador multissímbolo multiperíodo
Neste artigo, veremos um exemplo de como criar um indicador multissímbolo multiperíodo usando as classes das séries temporais da biblioteca DoEasy que exibe numa subjanela um gráfico mostrando o par de moedas selecionado no período gráfico desejado na forma de velas japonesas. Vamos modificar um pouco as classes da biblioteca e criar um arquivo separado para armazenar enumerações dos parâmetros de entrada dos programas e para a escolher da linguagem de compilação.
Trabalhando com séries temporais na biblioteca DoEasy (Parte 43): classes de objetos de buffers de indicador Trabalhando com séries temporais na biblioteca DoEasy (Parte 43): classes de objetos de buffers de indicador
Neste artigo, veremos a criação de classes de objetos-buffers de indicador como herdeiros de um objeto-buffer abstrato, o que simplifica a declaração e trabalho com buffers de indicadores ao criar programas-indicadores próprios baseados na biblioteca DoEasy.
Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos Monitoramento de sinais de negociação multimoeda (Parte 5): Sinais compostos
No quinto artigo relacionado à criação de um monitor de sinal de negociação, nós consideraremos os sinais compostos e implementaremos a funcionalidade necessária. Em versões anteriores, nós usamos os sinais simples, como o RSI, WPR e CCI, e também introduzimos a possibilidade de usar os indicadores personalizados.