English Русский 中文 Deutsch 日本語 Português
El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL (Parte 3). Diseñador de formas

El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL (Parte 3). Diseñador de formas

MetaTrader 5Ejemplos | 24 julio 2020, 15:05
892 0
Stanislav Korotky
Stanislav Korotky

En los primeros dos artículos (1, 2), analizamos el concepto general para la construcción de un sistema de marcado de interfaz en el lenguaje MQL, así como la implementación de las clases básicas que posibilitan la inicialización jerárquica de los elementos de la interfaz, su almacenamiento en la caché, su estilización, el ajuste de sus propiedades y el procesmaiento de sus eventos. La creación dinámica de elementos según la solicitud ha permitido cambiar sobre la marcha el aspecto de la disposición de la ventana de diálogo sencilla, mientras que la presencia de un repositorio único de elementos ya creados, nos ha dado de forma natural la posibilidad de guardarla en la sintaxis MQL propuesta, para su posterior inserción "como está" en el programa MQL donde se requiera la GUI. De esta forma, nos aproximamos a la creación de un editor gráfico de formas; en este artículo, vamos a trabajar a fondo en esa tarea.

Formulando la tarea

El editor debe posibilitar la ubicación de elementos en la ventana, así como el ajuste de sus propiedades básicas. Más abajo, mostramos la lista general de propiedades soportadas, pero no todas las propiedades están presentes en todos los tipos de elementos.

  • tipo,
  • nombre,
  • anchura,
  • altura,
  • estilo de alineación del contenido interno,
  • texto o encabezado,
  • color del fondo,
  • alineación en el contenedor padre,
  • sangrías/campos para los bordes del contenedor.

Aquí no hay muchas otras propiedades, por ejemplo, el nombre y el tamaño de la fuente, así como propiedades específicas de diferentes tipos de "controles" (en concreto, la propiedad de "botón especial"). Esto se ha hecho así a propósito, para simplificar el proyecto, cuya principal tarea es la prueba de concepto (proof of concept, POC). En caso necesario, podremos añadir al editor el soporte de propiedades adicionales más tarde.

El posicionamiento en coordenadas absolutas está disponible indirectamente a través de las sangrías, pero no está recomendado. El uso de contenedores CBox presupone que el posicionamiento deberá ser automáticamente ejecutado por los propios contenedores, de acuerdo con los ajustes de alineación.

El editor ha sido pensado para las clases de los elementos de interfaz de la Biblioteca Estándar. Para crear instrumentos similares para otras bibliotecas, necesitaremos escribir implementaciones concretas de todos los entes abstractos del sistema de marcado propuesto. En este caso, además, nos guiaremos por la implementación de las clases de marcado para la Biblioteca Estándar.

Debemos prestar atención a que el nombre "biblioteca de componentes estándar" no se corresponde del todo con la realidad, dado que, en el contexto de los artículos anteriores, ya tuvimos que modificarlo sustancialmente y sacarlo a una rama de versión paralela en la carpeta ControlsPlus. En el marco del presente artículo, continuaremos usándolo y modificándolo.

Vamos a enumerar los tipos de elementos que soportará el editor.

  • los contenedores CBox con orientación horizontal (CBoxH) y vertical (CBoxV),
  • el botón CButton,
  • el campo de edición CEdit,
  • la etiqueta CLabel,
  • el campo de edición con iteración de valores SpinEditResizable,
  • el calendario CDatePicker,
  • la lista desplegable ComboBoxResizable,
  • la lista ListViewResizable,
  • el grupo de interruptores independientes CheckGroupResizable,
  • el grupo de interruptores independientes RadioGroupResizable.

Todas las clases posibilitan el cambio adaptativo del tamaño (algunos tipos estándar ya tenían esta posibilidad inicialmente, para otros, hemos tenido que introducir nuestros propios cambios).

El programa constará de dos ventanas: la ventana de diálogo "Inspector", en la que el usuario seleccionará las propiedades necesarias de los elementos de control creados, y las formas "Diseñador", en la que precisamente se crean estos elementos, formando el aspecto externo de la interfaz gráfica proyectada.

Bosquejo de la interfaz del programa de diseño de la GUI MQL

Bosquejo de la interfaz del programa de diseño de la GUI MQL

Desde el punto de vista de MQL, en el programa tendremos 2 clases principales: InspectorDialog y DesignerForm, descritas en los archivos de encabezado homínimos.

  #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 ventanas son herederas de AppDialogResizable (después CAppDialog), formadas según la tecnología de marcado MQL. Por eso, vemos la llamada de CreateLayout, en lugar de Create.

Cada ventana tiene su propia caché de elementos de interfaz. Sin embargo, en el inspector, la ventana está llena desde el principio con los "controles" descritos en una disposición bastante compleja (que trataremos de analizar en líneas generales), mientras que en el diseñador está vacía. Esto se explica de forma muy sencilla: en el inspector se halla casi toda la lógica del programa, mientras que el diseñador sería un receptáculo en el que el inspector va implementando los nuevos elementos según los comandos del usuario.

Conjunto de propiedades PropertySet

Cada propiedad de la enumeradas se presenta con un valor de tipo concreto. Por ejemplo, el nombre de un elemento es una línea, mientras que la anchura y la altura son números enteros. El conjunto completo de valores describe el objeto que debe aparecer en el diseñador. Tiene sentido guardar el conjunto en un sitio; para ello, se ha creado la clase especial PropertySet. Pero, ¿qué variables de miembro debe haber en ella?

A primera vista, la solución más obvia parecería usar las variables de los tipos sencillos incorporados. No obstante, estas carecen de un rasgo importante que necesitaremos más tarde. MQL no soporta enlaces a las variables de tipo simple. Y los enlaces son muy útiles en los algoritmos de procesamiento de la interfaz de usuario. Aquí, con mucha frecuencia, se presupone la reacción compleja al cambio de valores. Por ejemplo, un cierto valor no permitido, introducido en uno de los campos, debe bloquear varios "controles" dependientes. Resultaría cómodo que estos "controles" pudieran gestionar su estado guiándose por un único lugar de almacenamiento del valor comprobado. Y resulta más cómodo hacer esto con la ayuda del "reparto" de enlaces a una misma variable. Por eso, en lugar de los tipos simples incorporados, usaremos una clase de envoltorio de plantilla con un aspecto bastante aproximado al que veremos ahora, con el nombre condicional Value.

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

La palabra "aproximado" no se menciona porque sí. En realidad, añadiremos a la clase cierta funcionalidad, de la que hablremos un poco más adelante.

La presencia del envoltorio de objeto nos permite captar la asignación de nuevos valores en el operador sobrecargado '=', lo cual resulta imposible al usar tipos simples. Y no vamos a necesitar esto.

Teniendo en cuenta esta clase, el conjunto de propiedades del objeto de interfaz se puede describir más o menos de esta forma:

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

En la ventana de diálogo del inspector se creará una variable de este clase como repositorio centralizado de los ajustes actuales, que van llegando desde los elementos de control del inspector.

Obviamente, para establecer en el inspector cada propiedad de las anteriormente enumeradas, se usa el elemento de control adecuado. Por ejemplo, para seleccionar el tipo de "control" creado, se usa la lista desplegable CComboBox, y para el nombre, el campo de edición CEdit. La propiedad supone un valor único de cierto tipo (línea, número, índice en la enumeración). Incluso aquellas propiedades que son compuestas, tales como las sangrías, definidas por separado para cada uno de los 4 lados, se deben analizar de forma independiente (izquierda, superior, etcétera), dado que, para introducirlas, se reservarán cuatro campos de edición, y, por consiguiente, cada magnitud estará relacionada con el elemento de control reservado para ella.

De esta forma, formaremos una regla obvia para la ventana de diálogo del inspector: en ella, cada elemento de control determina la propiedad vinculada con él, que tiene un valor concreto del tipo establecido. Esto nos llevará a la siguiente solución relacionada con la arquitectura.

Propiedades características de los "controles"

En los anteriores artículos, introdujimos la interfaz especial Notifiable, que permitía establecer el procesamiento de eventos para un elemento de control concreto.

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

Aquí, C es una de las clases de los "controles", por ejemplo, CEdit, CSpinEdit, etcétera. El procesador onEvent se llama automáticamente con la caché de disposición para los elementos y tipos correspondientes de los eventos. Obviamente, esto solo sucede si se introducen las líneas correctas en el mapa de procesamiento de eventos. Por ejemplo, en la parte anterior, construimos según este principio el procesamiento del botón "Inject" (fue descrita como heredero de Notifiable<CButton>).

En los casos en los que un elemento de control se usa para ajustar las propiedades de tipo determinado, se hace necesario crear una interfaz más especializada de PlainTypeNotifiable.

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

La misión del método value es retornar de un elemento del tipo C el valor del tipo V más característico para C. Por ejemplo, para la clase CEdit, resultaría un modo natural retornar un valor del tipo string (en una cierta clase ExtendedEdit hipotética).

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

Para cada tipo de "control" existe un tipo de datos único y característico o un círculo limitado de los mismos (por ejemplo, para los números enteros, podemos seleccionar la exactitud short, int, long). Y todos los "controles" tienen este u otro método "getter", preparado para ofrecer un valor en el método value redefinido.

Así, hemos llegado a la esencia de la solución de arquitectura: la vinculación mutua de las clase Value y PlainTypeNotifiable. Se implementa con la ayuda de la clase heredera PlainTypeNotifiable, que toma el valor de un "control" desde el inspector y lo ubica en la propiedad Value vinculada a él.

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

Gracias a la herencia de la clase de plantilla PlainTypeNotifiable, la nueva clase NotifiableProperty supone al mismo tiempo una clase del "control" C y un proveedor de valores del tipo V.

El método bind permite mantener dentro del "control" el enlace a Value, y después cambiar el valor de la propiedad según el lugar (según el enlace) de forma automática en respuesta a las acciones del usuario con el "control".

Por ejemplo, para los campos de edición de tipo string, se ha introducido la propiedad EditProperty, similar al ejemplo ExtendedEdit, pero heredada de NotifiableProperty:

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

Para la lista desplegable, la clase análoga describe una propiedad con un valor de tipo entero.

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

En el programa, se describen las clases de los "controles"-propiedades para todos los principales tipos de elementos.

Diagrama de clases de las "propiedades notificables"

Diagrama de clases de las "propiedades notificables"

Ahora, ha llegado el momento de deshacerse del epíteto "aproximado" y familiarizarse con las clases completas.

StdValue — valor, observación, dependencia

Un poco más arriba, ya mencionamos la situación estándar en la que se requiere monitorear el cambio de algunos "controles" para comprobar la admisibilidad y el cambio del estado de otros "controles". En otras palabras, necesitamos un cierto observador capaz de monitorear el estado de un "control" y comunicar sus cambios a otros "controles" interesados.

Con este objetivo, hemos introducido la interfaz StateMonitor (observador).

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

El método notify ha sido pensado para que la fuente invoque ciertos cambios, de manera que este observador pueda reeaccionar de forma correspondiente (si así se requiere). La fuente de los cambios se puede identificar por el parámetro sender. Obviamente, la fuente de los cambios debe saber preliminarmente de alguna manera que un observador concreto está interesado en recibir notificaciones. Con este objetivo, la fuente debe implementar la interfaz CDatabaseWriter.

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

Con la ayuda del método subscribe, el observador puede transmitir al "editor" un enlace a sí mismo. No resulta difícil suponer que, en nuestro caso, las fuentes de cambio serán propiedades, y por eso, la clase Value hipotética, en realidad, será heredada de Publisher y tendrá el aspecto siguiente.

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

Cualquier observador que se registre entrará en la matriz dependencies, y al modificarse la magnitud, se le notificará llamando su método notify.

Dado que las propiedades están vinculadas de forma unívoca con los "controles" que ayudan a introducirlas, hemos previsto el guardado del enlace a un "control" en la clase de propiedades definitiva para la Biblioteca Estándar, StdValue (usa el tipo básico de todos los "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;
      }
  };

Necesitaremos este enlace más tarde.

Precisamente las instancias StdValue rellenan PropertySet.

Diagrama de conexiones de StdValue

Diagrama de conexiones de StdValue

En la clase NotifiableProperty, mencionada anteriormente, también se utiliza en realidad StdValue; asimismo, en el método bind, implementamos la vinculación del valor de propiedad al "control" (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();
      }
      ...
  };

Control automático del estado de los "controles":EnableStateMonitor

El método más demandado para reaccionar al cambio de ciertos ajustes es el bloqueo o desbloqueo de otros "controles" independientes. El estado de cada uno de estos "controles" adaptables puede depender de varios ajustes (no solo de uno obligatoriamente). Para monitorear estos, hemos desarrollado la clase abstracta 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;
  };

El "control" cuyo estado es monitoreado por este observador se ubica en el campo "control". La matriz sources contiene las fuentes de cambio que influyen en el estado. La matriz debe rellenarse en las clases herederas. Cuando conectamos un observador a un "control" concreto llamando a attach, el observador se suscribe a todas las fuentes de cambio. A continuación, comenzará a obtener de forma operativa las notificaciones sobre los cambios en las fuentes a través de las llamadas de su método notify.

El método isEnabled decide si es necesario bloquear o desbloquear un "control", pero dicho método aquí se declara como abstracto, y se implementará en las clases herederas.

Para las clases de la Biblioteca Estándar, conocemos el mecanismo de bloqueo de "controles" con la ayuda de los métodos generales Enable y Disable. Vamos a utilizar estos para implementar la clase concreta EnableStateMonitor.

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

En la práctica, esta clase se usará en el programa en muchos casos, pero solo vamos a analizar un ejemplo. Para crear nuevos objetos o aplicar propiedades modificadas en el diseñador, en la ventana de diálogo del inspector tenemos el botón Apply (para él, hemos definido la clase ApplyButton, derivada de Notifiable<CButton>).

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

Dicho botón deberá bloquearse si el nombre del objeto no ha sido establecido, o si no se ha elegido su tipo. Por eso, nosotros implementamos ApplyButtonStateMonitor con dos fuentes de cambio ("editores"): el nombre y el 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";
      }
  };

El constructor de la clase aplica los dos parámetros que indican las propiedades correspondientes. Se guardan en la matriz sources. En el método isEnabled, se comprueba si se ha rellenado el nombre y se ha seleccionado el tipo (no es igual a -1). Si las condiciones se cumplen, se permitirá pulsar el botón. El nombre se comprueba adicionalmente en la línea "Client", reservada en las ventanas de diálogo de la Biblioteca Estándar más allá de la zona de cliente, y por eso no se puede encontrar en el nombre de los elementos de usuario.

En la clase de la ventana de diálogo del inspector, tenemos una variable del tipo ApplyButtonStateMonitor, que se inicializa en el constructor con los enlaces a los objetos StdValue, que guardan el nombre y el tipo.

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

En la disposición del diálogo, las propiedades del nombre y el tipo están vinculadas a los "controles" correspondientes, mientras que el observador está vinculado al botón 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());

El método attach en el objeto applyMonitor ya nos resulta familiar, pero el método attach en los objetos de disposición _layout sí que es algo nuevo. La clase _layout fue analizada con detalle en el segundo artículo, y el método attach es el único cambio en comparación con aquella versión. Este método intermediario simplemente llama a bind para el elemento de control generado por el objeto _layout dentro de la ventana de diálogo del inspector.

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

Recordemos que todos los "controles" de propiedad (incluidos EditProperty y ComboBoxProperty, como en este ejemplo) son herederos de la clase NotifiableProperty en la que se encuentra el método bind para vincular los "controles" a las variables StdValue que guardan las propiedades correspondientes. De esta forma, los "controles" en la ventana del inspector resultan vinculados a las propiedades correspondientes, mientras que aquellos, a su vez, son monitoreados por el observador ApplyButtonStateMonitor. En cuanto el usuario cambia el valor de cualquiera de los dos campos, esto se representa en el conjunto de la propiedad PropertySet (recordemos el manejador onEvent para los eventos ON_CHANGE y ON_END_EDIT en NotifiableProperty) y se notifica a los observadores registrados, incluido ApplyButtonStateMonitor. Como resultado de ello, el estado de este botón cambia automáticamente al actual.

En la ventana de diálogo del instructor se necesitan varios monitores que controlen el estado de los "controles" según un principio semejante. Describiremos las reglas concretas del bloqueo en el apartado de las instrucciones para el usuario.

Clases StateMonitor

Clases StateMonitor

Bien, vamos a marcar la correspondencia definitiva entre todas las propiedades del objeto creado y los "controles" en la ventana de diálogo del inspector.

  • nombre — EditProperty, línea;
  • tipo — ComboBoxProperty, número entero, número del tipo de la lista de elementos soportados;
  • anchura — SpinEditPropertySize, número entero, píxeles;
  • altura — SpinEditPropertySize, número entero, píxeles;
  • estilo — ComboBoxProperty, número entero igual al valor de una de las enumeraciones (dependiendo del tipo de elemento): VERTICAL_ALIGN (CBoxV), HORIZONTAL_ALIGN (CBoxH), ENUM_ALIGN_MODE (CEdit);
  • texto — EditProperty, línea;
  • color de fondo — ComboBoxColorProperty, valor de color de la lista;
  • alineación de los bordes — AlignCheckGroupProperty, máscara de bits, grupo de banderas independientes (ENUM_WND_ALIGN_FLAGS + WND_ALIGN_CONTENT);
  • sangrías — cuatro SpinEditPropertyShort, números enteros;

El nombre de las clases de algunas "Property" indica su especialización, es decir, la ampliación de la funcionalidad en comparación con la implementación básica ofrecida por las variedades "simples" de SpinEditProperty, ComboBoxProperty, CheckGroupProperty, etcétera. Entenderemos para qué son necesarias a partir de las instrucciones de usuario.

Para representar estos "controles" de forma cuidadosa y visual, el marcado de la ventana de diálogo, por supuesto, incluye contenedores y marcas informativas adicionales. Podrá familiarizarse con el código completo en los archivos adjuntos.

Procesamiento de eventos

El procesamiento de eventos de todos los "controles" se define en el 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 aumentar la efectividad del procesamiento de los eventos en la caché, hemos adoptado algunas medidas especiales. Las macros ON_EVENT_LAYOUT_CTRL_ANY y ON_EVENT_LAYOUT_CTRL_DLG, introducidas en el segundo artículo, basan su funcionamiento en la búsqueda de un "control" en la matriz de la caché según el número único obtenido del sistema en el parámetro lparam. En este caso, además, la implementación de la caché realiza la búsqueda lineal por la matriz.

Para acelerar el proceso en la clase MyStdLayoutCache (heredera de StdLayoutCache) cuya instancia se guarda y utiliza en el inspector, hemos añadido el método buildIndex. La posibilidad de realizar una cómoda indexación implementada en él, se apoya en la propiedad particular de la Biblioteca Estándar que permite asignar números únicos a todos los elementos. En el método CAppDialog::Run, se selecciona un número aleatorio, la m_instance_id que ya conocemos y a partir de la cual se numeran todos los objetos del gráfico creados por la ventana. Así, podemos conocer el intervalo de los valores obtenidos. Excepto m_instance_id, cada valor de lparam que llega con el evento se convierte en un número directo del objeto. No obstante, el programa crea en el gráfico bastantes más objetos de los guardados en la caché, porque muchos "controles" (y la propia ventana, como un conjunto de marco, encabezado, botones de minimización, ectétera) constan de multitud de objetos de nivel bajo. Por eso, el índice en la caché nunca coincide con el identificador del objeto menos la m_instance_id. Debido a ello, hemos tenido que reservar una matriz de índices especial (su tamaño es igual al número de objetos en la ventana), anotando de alguna forma para aquellos "controles" "reales" que hay en la caché sus números ordinales en esta. Como resultado, el acceso se proporciona prácticamente al instante, según el principio de direccionamiento indirecto.

Las matrices se rellenarán solo después de que la implementación básica de CAppDialog::Run asigne los números únicos, pero antes de que el manejador OnInit finalice su trabajo. Lo mejor para lograr estos objetivos, será hacer virtual el método Run (en la Biblioteca Estándar es precisamente así) y redefinir en InspectorDialog, por ejemplo, así:

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

El propio buildIndex es bastante sencillo.

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

Ahora, podemos escribir una implementación rápida del método de búsqueda de "controles" según su 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);
      }

Bueno, ya hemos hablado suficiente sobre la construcción interna del inspector.

Este es el aspecto que tiene su ventana en el programa iniciado.

Ventana de diálogo de Inspector y forma de Designer

Ventana de diálogo de Inspector y forma de Designer

Aparte de las propiedades, aquí podemos ver ciertos elementos conocidos. Todos ellos serán descritos más tarde. Por ahora, vamos a prestar atención al botón Apply. Después de que el usuario asigne los valores a las propiedades, podrá generar el objeto solicitado en la forma del diseñador pulsando este botón. Disponiendo de la clase derivada de Notifiable, el botón es capaz de procesar la pulsación en su propio 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;
      };
  };

Recordemos que las variables inspector y designer son objetos globales con la ventana de diálogo del inspector y la forma del diseñador, respectivamente. El inspector tiene en su interfaz de programa el método getProperties, para proporcionar el conjunto actual de propiedades PropertySet, descrito más arriba:

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

PropertySet sabe comprimirse en la estructura plana (normal) Properties para transmitirse al método inject del diseñador. Y aquí, pasamos suavemente a la ventana del diseñador.

Diseñador

Si omitimos las comprobaciones auxiliares, la esencia del método inject se parece a lo que vimos al final del segundo artículo: la forma coloca en la pila de la disposición el contenedor meta (en el segundo artículo, se estableció como estático, es decir, siemrpe era el mismo), generando en él el elemento con las propiedades transmitidas. En la nueva forma, todos los elementos se pueden destacar clicando con el ratón, cambiando con ello el contexto de inserción. Además, al clicar, se inicia el traslado de las propiedades del elemento destacado al inspector. De esta forma, tendremos la posibilidad de editar las propiedades de los objetos ya creados y actualizarlas con la ayuda del propio botón Apply. El diseñador determina si el usuario quiere introducir un nuevo elemento o editar una antiguo comparando el nombre y el tipo del elemento. Si ya existe esa combinación en la caché del diseñador, indicará que estamos hablando de la edición.

Veamos qué aspecto tiene en líneas generales la adición de un nuevo 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);
        }
      }

La variable "cache" se describe en DesignerForm y contiene el objeto de clase DefaultStdLayoutCache, derivado de StdLayoutCache (lo mostramos en artículos anteriores). StdLayoutCache permite, con la ayuda del método get, encontrar un objeto según su nombre. Si no lo hay, estaremos hablando de un objeto nuevo, y el diseñador intentará determinar el contenedor actual destacado por el usuario. Para ello, hemos implementado el método getSelected en la nueva clase DefaultStdLayoutCache. Un poco más tarde, veremos exactamente cómo se hace. Aquí, es importante destacar que el lugar de implementación del nuevo elemento puede ser solo un contenedor (en nuestro caso, se utilizan los contenedores de la familia CBox). Si en este momento no está seleccionado un contenedor, el algoritmo llamará a findParent para determinar el contenedor padre y utilizarlo como objetivo. Una vez se ha determinado el lugar de inserción, comienza a funcionar el esquema acostumbrado de marcado con bloques incorporados. En el bloque externo, se crea el objeto _layout con el contenedor objetivo, generándose después en el interior el objeto, en la línea:

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

Todas las propiedades se transmiten al método auxiliar getPtr. Este sabe crear objetos de todos los tipos soportados, pero, para mayor sencillez, vamos a mostrar qué aspecto tiene solo para algunos de ellos.

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

Los objetos _layout, convertidos en plantilla por el tipo establecido de elemento GUI, se crean con la ayuda de los constructores que ya conocemos por las descripciones estáticas de los marcados MQL. Los objetos _layout ofrecen la posibilidad de utilizar operadores sobrecargados <= para establecer las propiedades, en concreto, así se rellena el estilo HORIZONTAL_ALIGN para CBoxH, ENUM_ALIGN_MODE para el campo de texto o los intervalos del "spinner". Los ajustes de las demás propiedades generales (sangrías, texto, color) se delegan en el método auxiliar applyProperties (el lector podrá analizarlo con detalle en los archivos adjuntos).

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

Cuando el objeto ha sido localizado en la caché por el nombre, sucede lo siguiente (aspecto simplificado):

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

El método auxiliar update traslada las propiedades desde la estructura "props" al 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;
      }
    }

Ahora, volvamos a la selección de elementos de GUI en la forma. Su solución corre a cargo del objeto de caché, que procesa los eventos iniciados por el usuario. En la clase StdLayoutCache, se ha reservado el manejador onEvent, que se conecta a los eventos del gráfico con la ayuda de la macro ON_EVENT_LAYOUT_ARRAY:

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

Esto envía la pulsación del ratón para todos los elementos de la caché al manejador onEvent, que determinamos en nuestra clase derivada DefaultStdLayoutCache. En la clase, hemos creado el puntero selected del tipo universal de ventana CWnd; debe ser rellenado por el manejador 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;
      }
  };

La selección visual de un elemento en la forma se realiza con la ayuda del marco rojo en el método trivial highlight (llamando ColorBorder). El manejador primero quita la selección del anterior elemento elegido (establece el color del marco CONTROLS_BUTTON_COLOR_BORDER), luego encuentra el elemento de la caché que se corresponde con el objeto del gráfico en el que se ha realizado la pulsación, y registra el puntero al mismo en la variable selected. Finalmente, el nuevo objeto seleccionado se marca con un marco rojo, después de lo cual, el evento ON_LAYOUT_SELECTION es enviado al gráfico. Todo ello comunica al inspector que en la forma se ha destacado un nuevo elemento, motivo por el cual se deben mostrar sus propiedades en la venatana de diálogo del inspector.

En el inspector, este evento se capta en el manejador OnRemoteSelection, que solicita al diseñador el enlace al objeto destacado y lee en él todos los atributos a través de la API estándar de la biblioteca.

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

Aquí está el inicio del 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();
      ...
    }
  }

Tras recibir de la caché del diseñador el enlace ptr al objeto destacado, el algoritmo averigua su nombre, lo limpia del identificador de la ventana (este campo m_instance_id en la clase CAppDialog es un sufijo en todos los nombres, para que no surjan conflictos entre los objetos de distintas ventanas, y tenemos 2 de ellas), y lo registra en el "control" relacionado con el nombre. Preste atención a que precisamente aquí usamos el enlace inverso al "control" (backlink()) de la propiedad StdValue<string> name. Además, como estamos modificando el campo desde dentro, el evento sobre su modificación no se generará (como sucede cuando el usuario actúa como iniciador del cambio), y por eso se requerirá adicionalmente registrar el nuevo valor en la propiedad correspondiente PropertySet (props.name).

En teoría, con las posiciones de la POO sería más correcto redefinir para cada tipo de "control" de propiedad su método virtual de cambio y actualizar automáticamente la instancia de StdValue vinculada a él. Veamos cómo se puede hacer esto para CEdit, por ejemplo.

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

Entonces, el cambio del contenido con la ayuda del método Text() provocaría la posterior llamada de OnSetText y la actualización automática de property. Pero esto no resulta demasiado cómodo de hacer para los controles compuestos como CCheckGroup, por eso hemos dado preferencia a una implementación más utilitaria.

De forma similar, con la ayuda de enlaces inversos a los "controles", actualizamos el contenido en los campos de altura, anchura tipo y otras propiedades del objeto destacado en el diseñador.

Para identificar los tipos soportados, tenemos una enumeración cuyo elemento se puede determinar partiendo de la variable especial _rtti, que añadimos en los artículos anteriores en el nivel más inferior, en la clase CWnd, y que rellenamos en todas las clases derivadas con el nombre de la clase concreta.

Instrucciones del usuario

La ventana de diálogo del inspector contiene campos de edición de diferentes tipos con las propiedades del objeto actual (destacado en el diseñador) o el objeto que se supone que vamos a crear.

Los campos a rellenar obligatoriamente son el nombre (línea) y el tipo (se selecciona en una lista desplegable).

Los campos de altura y anchura nos permiten establecer el tamaño del objeto en píxeles. No obstante, estos ajustes no se tienen en cuenta si se indica más abajo un modo específico de expansión: por ejemplo, la vinculación a los bordes izquiero y derecho indican la anchura según el tamaño del contenedor. Clicando con el ratón en el campo de altura o anchura con la tecla Shift pulsada, podemos resetear la propiedad por defecto (anchura — 100, altura — 20).

Todos los "controles" del tipo SpinEdit (no solo en las propiedades de tamaño) han sido perfeccionados de tal forma que el desplazamiento del ratón dentro del "control" hacia la izquierda o hacia la derecha con el botón del ratón presionado (drag, pero no drop) realizan el cambio rápido de los valores del "spinner" proporcionalmente a la distancia superada en píxeles. Se ha hecho así para facilitar la edición, que no resulta muy cómoda de ejecutar pulsando botones diminutos. Los cambios están disponibles para cualquier programa que comience a usar los "controles" de la carpeta ControlsPlus.

La lista desplegable con los estilos de alineación del contenido (Style) está solo disponible para los elementos CBoxV, CBoxH y CEdit (para los demás tipos, se bloquea). Para los contenedores CBox, se han implicado todos los modos de alineación ("center", "justify", "left/top", "right/bottom", "stack"). Para CEdit, funcionan solo los que se corresponden con ENUM_ALIGN_MODE ("center", "left", "right").

El campo Text permite establecer el encabezado del botón CButton, la etiqueta CLabel o el contenido CEdit. Para los demás tipos, el campo permanece inactivo.

La lista desplegable Color ha sido diseñada para seleccionar el color de fondo de la lista de colores Web. Está disponible solo para CBoxH, CBoxV, CButton y CEdit. Los demás tipos de "controles", al ser compuestos, requieren de una técnica más elaborada en cuanto a la actualización del color en todos sus componentes, por eso, hemos decidido no ofrecer soporte por el momento al coloreado de estos. Para seleccionar el color, se ha modificado la clase CListView. Hemos añadido a la misma un modo especial "de color" en el que los valores de los elementos de la lista se tratan como códigos de color, y el fondo de cada elemento se dibuja del color correspondiente. Este modo se activa con el método SetColorMode, y se usa en la nueva clase ComboBoxWebColors (especialización ComboBoxResizable de la carpeta Layouts).

Los colores estándar de GUI de la biblioteca no se pueden seleccionar en este momento, porque existe un problema con la definición de los colores por defecto. Para nosotros, es importante conocer el color por defecto para cada tipo de "controles", para no mostrarlo destacado en la lista cuando el usuario no ha seleccionado ningún color especial. El enfoque más sencillo consiste en crear un "control" vacío de un tipo concreto y leer su propiedad ColorBackground, pero esto funciona con un número de "controles" muy limitado. Lo que ocurre es que el color, normalmente, no se asigna en el constructor de la clase, sino en el método Create, que implica mucha inicialización innecesaria, incluyendo la creación de objetos reales en el gráfico. Y, claro está, no necesitamos objetos sobrantes que no vamos a usar. Además, el color del fondo de muchos objetos compuestos se obtiene del fondo del substrato, y no del "control" principal. Debido a la complejidad que entraña considerar todos estos detalles, hemos decido considerar como no seleccionados todos los colores utilizados por defecto en cualquier clase de "control" de la Biblioteca Estándar. Y esto significa que no podemos incluirlos en la lista, dado que, en caso contrario, el usuario podría seleccionar uno de dichos colores, pero, como resultado de ello, no vería confirmada su selección en el inspector. Las listas de los colores web y los colores estándar de GUI se muestran en el archivo LayoutColors.mqh.

Para resetear los colores a su valor por defecto (distinto para cada tipo de "control"), debemos seleccionar la primera variante "vacía" de la lista que se corresponda con clrNONE.

Las banderas de los interruptores Alignment independientes se corresponden con los modos de alineación lateral de la lista ENUM_WND_ALIGN_FLAGS; además, se les ha añadido el modo especial WND_ALIGN_CONTENT, descrito en el segundo artículo y operativo solo para los contenedores. Si al pulsar algún interruptor, mantenemos pulsado la tecla Shift, el programa conmutará sincrónicamente las 4 banderas ENUM_WND_ALIGN_FLAGS. Si la opción es activada, las demás también lo serán, y al contrario, si la opción es desactivada, las demás también serán reseteadas. Esto nos permite conmutar con un solo clic todo el grupo, excepto WND_ALIGN_CONTENT.

Los "spinners" Margins establecen las sangrías respecto a los lados del rectángulo del contenedor en el que se encuentra este elemento. Orden de los campos: izquierdo, superior, derecho, inferior. Es posible poner a cero rápidamente todos los campos, pulsando sobre cualquier campo con la tecla Shift pulsada. También podemos establecer fácilmente todos los campos como iguales, clicando sobre el campo con el valor necesario con la tecla Ctrl pulsada; como resultado, el valor será copiado a los otros 3 campos.

Ya estamos familiarizados con el botón Apply: este aplica los cambios introducidos, dando como resultado que en el diseñador o bien se cree un nuevo "control", o bien se modifique el antiguo.

La inserción de un nuevo objeto se realiza en el objeto de contenedor destacado, o en el contenedor que contenga el "control" destacado (si se ha seleccionado "control").

Para destacar un elemento en el diseñador, deberemos clicar sobre él con el ratón. El elemento destacado se ilumina con un marco rojo. La única excepción será CLabel, que no da soporte a esta posibilidad.

El nuevo elemento se destaca inmediatamente después de ser insertado.

En la ventana de diálogo vacía, podemos insertar solo un contenedor CBoxV o CBoxH, además, en este caso, no es obligatorio destacar la zona de cliente. Este primer contenedor, el más grande, se expande por defecto por toda la ventana.

Un nuevo clic sobre el elemento ya destacado solicitará la eliminación. La eliminación tendrá lugar después de que el usuario confirme esta.

El botón TestMode, de dos posiciones, conmuta entre los dos modos de trabajo del diseñador. Por defecto, se encuentra pulsado, el modo de prueba está desactivado, y funciona la edición de la interfaz del diseñador: el usuario puede destacar los elementos clicando con el ratón, y también eliminarlos. En el estado pulsado, se activa el modo de simulación. En este caso, además, la ventana de diálogo funciona aproximadamente de la misma forma en que lo hará en el programa actual; la edición de la disposición y el resaltado de elementos están desactivados.

El botón Export ofrece la posibilidad de guardar la configuración actual de la interfaz del diseñador de la disposición MQL. El nombre del archivo con el prefijo layout contiene la plantilla actual de la hora y la extensión txt. Si, al pulsar Export, dejamos la tecla Shift pulsada, la configuración de la forma no se guardará como texto, sino como tipo binario, en un archivo de formato propio con la extensión mql. Esto resulta cómodo porque se puede interrumpir el progreso del proyecto en cualquier momento, reanudando el mismo después de un tiempo. Para cargar un archivo mql binario de disposición, se usa el mismo botón Export, con la condición de que la forma y la caché estén vacías, lo cual se ejecuta justo después de iniciar el programa. La versión actual siempre intenta importar el archivo "layout.mql". Si el lector lo desea, podrá implementar la selección del archivo en los parámetros de entrada, o en MQL.

En la parte superior de la ventana de diálogo del inspector, se ubica una lista desplegable con todos los elementos creados en el diseñador. La selección de un elemento de la lista conllevará que este sea automáticamente elegido y resaltado en el diseñador. Y al contrario: el resaltado de un elemento en la forma hará que este sea el actual en la lista.

Ahora, durante la edición, pueden surgir 2 categorías de errores: aquellos que se pueden corregir analizando el marcado MQL, y otros más serios. A la primera categoría pertenecen las combinaciones de ajustes con las que "controles" o contenedores se salen del marco de la ventana o del contenedor padre. En este caso, normalmente, dejan de destacarse con el ratón, y solo se los puede hacer activos con la ayuda del selector en el inspector. A la hora de averiguar qué propiedades precisamente son erróneas, puede resultarnos útil el análisis del marcado MQL en forma de texto: para obtener su estado, basta con pulsar el botón Export. Después de analizar el marcado, deberemos corregir las propiedades en el inspector, restaurando con ello el aspecto correcto de la forma.

Esta versión del programa se ha pensado para comprobar el concepto, por lo que en el código fuente no hay comprobaciones de todas las combinaciones de parámetros que pueden aparecer al recalcular el tamaño de los contenedores adaptables.

A la segunda categoría de errores pertenece concretamente la situación en la que algún elemento ha sido insertado por error en el contenedor equivocado. En este caso, solo podemos eliminar el elemento y añadirlo de nuevo, pero ya en otro sitio.

Sería recomendable guardar la forma en formato binario (botón Export con la tecla Shift pulsada) para poder continuar trabajando con la última configuración buena, en caso de que se dieran problemas irresolubles.

Vamos a analizar algunos ejemplos de trabajo con el programa.

Ejemplos

Primero, intentaremos reproducir en el diseñador la estructura del inspector. En la siguiente imagen animada, se muestra el inicio del proceso con la adición de las cuatro líneas superiores y los campos de definición del nombre, el tipo y la anchura. Se usan distintos tipos de "controles", alineaciones, composición de color. Las etiquetas con los nombres de los campos se forman con la ayuda del campo de edición CEdit, porque CLabel tiene una funcionalidad muy limitada (en concreto, no ofrece soporte para la alineación del texto, ni color de fondo). Sin embargo, en el inspector no está disponible el ajuste del atributo "solo lectura", por eso, la única manera de marcar la etiqueta como no editable, es asignar el fondo gris (se trata simplemente de un efecto visual). En el código MQL, semejantes objetos CEdit, claro está, deben ser ajustados adicionalmente de la forma correspondiente, es decir, pasar al modo "solo lectura". Precisamente eso hemos hecho en el propio inspector.

Proceso de edición de la forma

Proceso de edición de la forma

La edición de la forma muestra claramente el carácter adaptativo de la tecnología de marcado, y cómo el aspecto exterior está unívocamente relacionado con el marcado MQL. En cualquier momento, podemos pulsar el botón Export y ver el código MQL resultante.

En la variante final, es posible obtener la ventana de diálogo prácticamente en toda la ventana resultante del inspector (con la excepción de algunos detalles).

Marcado de la ventana de diálogo Inspector recreado en el diseñador

Marcado de la ventana de diálogo Inspector recreado en el diseñador

No obstante, debemos recordar que dentro del inspector hay muchas clases de "controles" que no son estándar, ya que han sido heredadas de esta u otra propiedad x-Property y ofrecen un envoltorio algorítmico adicional. En nuestro ejemplo, en el diseñador se usan solo clases estándar de "controles" (ControlsPlus). En otras palabras, la disposición obtenida siempre contiene solo la representación externa del programa y el comportamiento estándar de los "controles". El seguimiento del estado de los elementos y la codificación de la reacción a sus cambios, incluyendo la posibilidad de personalizar las clases, es la prerrogativa del programador. El sistema creado permite cambiar las entidades en el marcado MQL como en el MQL habitual. Es decir, podemos sustituir, por ejemplo, ComboBox por ComboBoxWebColors. Pero, sea como fuere, todas las clases mencionadas en la disposición deberán estar conectadas al proyecto con la ayuda de la directiva #include.

La ventana de diálogo presentada anteriormente (el duplicado del inspector) ha sido guardada con la ayuda del comando Export en dos archivos, de texto y binario: ambos se adjuntan al artículo con los nombres layout-inspector.txt y layout-inspector.mql, respectivamente.

Tras analizar el archivo de texto, podemos comprender la esencia del marcado del inspector sin vinculación a los algoritmos y a los datos.

En principio, después de exportar el marcado a un archivo, su contenido se puede insertar en cualquier proyecto al que estén conectados los archivos de encabezado del sistema de disposición y todas la clases de GUI utilizadas. Como resultado, obtendremos la parte dedicada a la interfaz de trabajo. En concreto, adjuntamos al artículo un proyecto con una ventana de diálogo vacía DummyForm. Si el lector lo desea, podrá encontrar en ella CreateLayout e insertar en este el marcado MQL que se preparará preliminarmente en el diseñador.

Resulta igualmente sencillo de hacer para layout-inspector.txt. Copiamos en el portapapeles el contenido completo de este archivo y lo pegamos en el archivo DummyForm.mqh, dentro del método CreateLayout, donde se encuentra el comentario "// insert exported MQL-layout here".

Tenga cuenta que, en la representación de texto de la disposición, existe una mención al tamaño de la ventana de diálogo (en este caso, 200*350) conforme a la cual se ha creado. Por eso, en el código fuente de CreateLayout, después de la línea para crear el objeto con la forma _layout<DummyForm> dialog(this...) y antes de la disposición copiada, deberemos insertar las líneas:

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

Esto proporcionará suficiente espacio a todos los "controles", y no permitirá hacer más pequeña la ventana de diálogo.

Nosotros no generamos el fragmento correspondiente de forma automática al realizar la exportación, por eso, la disposición puede representar solo una parte de la ventana de diálogo, o, en perspectiva, prestar servicio a otras clases de las ventanas y contenedores donde no se encuentren estos métodos.

Si compilamos e iniciamos ahora el ejemplo, conseguiremos una copia muy semejante del inspector. Pero, aun así, existen ciertas diferencias.

Interfaz del inspector recreada

Interfaz del inspector recreada

En primer lugar, todas las listas desplegables están vacías, y por eso no funcionan. Ningún "spinner" está configurado ni funciona. El grupo de banderas de alineación está visualmente vacío porque en la disposición no hemos generado ni una sola casilla de verificación, pero el "control" correspondiente existe, y en él hay incluso 5 casillas de verificación ocultas, generadas por la biblioteca de componentes estándar partiendo del tamaño inicial del "control" (todos estos objetos se pueden ver en la lista de objetos del gráfico, comando Object List).

En segundo lugar, el grupo de "spinners" con los valores de las sangrías realmente no existe, lo hemos trasladado a la forma porque en el inspector se crea con un objeto de disposición como matriz. Nuestro editor no tiene tal cosa. Podríamos crear 4 elementos independientes, pero entonces tendríamos que ajustarlos todos en el código de forma similar unos respecto a otros.

Al pulsar cualquier "control", la forma muestra en el log su nombre, clase e identificador.

También podemos cargar el archivo binario layout-inspector.mql (renombrándolo previamente como layout.mql) de vuelta al inspector y continuar editándolo. Para ello, bastará con iniciar el proyecto principal y pulsar Export con la forma aún vacía.

Tenga en cuenta que el diseñador genera -para mayor claridad- un cierto número de datos para todos los "controles" con listas o grupos, y que también establece el intervalo de los spinners. Por eso, al conmutar a TestMode, podemos "jugar" con los elementos. Este tamaño de los pseudo-datos se establece en la forma del diseñador con la macro DUMMY_ITEM_NUMBER, y es igual por defecto a 11.

Ahora, veremos el aspecto que podría tener el panel comercial en el diseñador.

Maqueta del panel comercial Color-Cube-Trade-Panel

Maqueta del panel comercial Color-Cube-Trade-Panel

No pretende ser una funcionalidad magnífica, pero su punto fuerte reside en que puede ser modificada fácilmente de manera casi ilimitada, de acuerdo con las preferencias de cualquier tráder concreto. Esta forma, al igual que la anterior, usa contenedores de colores distintos, para comprender con mayor facilidad su ubicación.

Insistimos una vez más en que hablamos solo de su aspecto externo. En la salida del diseñador, obtenemos un código MQL que se encarga solo de generar la ventana y el estado inicial de los "controles". Todos los algoritmos de cálculo, la reacción a las acciones del usuario, la protección contra los datos introducidos de forma incorrecta y el envío de órdenes comerciales, como siempre, deberán ser programados manualmente.

En esta maqueta, queda cambiar algunos tipos de "controles" por algo más adecuado. Así, las fechas de expiración de las órdenes pendientes se marcan ella se marcan con el "Calendario", y este no da soporte a la introducción de la fecha y la hora. Todas las listas desplegables deberán ser rellenadas con las variantes correspondientes, por ejemplo, los niveles stop se pueden introducir en unidades diferentes (precio, distancia en puntos, riesgo (pérdidas) en porcentaje del depósito o magnitud absoluta); el volumen puede establecerse como fijo, en dinero o como porcentaje del margen libre, etcétera; el trailing stop es uno de los diferentes algoritmos.

Este marcado se adjunta al artículo como dos archivos layout-color-cube-trade-panel: de texto y binario. El primero se puede introducir en una forma vacía (como DummyForm) y rellenarse con los datos y el procesamiento de eventos. El segundo, se puede cargar en el diseñador para su edición. Pero no olvide que el editor gráfico no es obligatorio. También podrá corregir el marcado en la representación textual. La ventaja del editor consiste en que podemos jugar con los ajustes y ver los cambios sobre la marcha. No obstante, solo soporta las propiedades más básicas.

Conclusión

En el presente artículo, hemos analizado un editor sencillo para el desarrollo interactivo de la interfaz gráfica de programas construidos con la tecnología de marcado MQL. La implementación propuesta incluye solo las posibilidades básicas, pero aun así suficientes para mostrar la funcionalidad del concepto y su posterior extensión a otros tipos de "controles", así como el futuro soporte de diferentes propiedades, de otras bibliotecas de componentes de GUI y sus mecanismos de edición. En concreto, en el editor falta por el momento la función de cancelación de operaciones, la inserción de elementos en una posición aleatoria en el contenedor (es decir, no solo la aidición al final de la lista de "controles" ya existentes) de las operaciones grupales, el copiado y pegado desde el portapapeles y mucho más. No obstante, los códigos fuente descubiertos permiten completar y adaptar la tecnología según las propias necesidades.

Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/7795

Archivos adjuntos |
MQL5GUI3.zip (112.66 KB)
Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura Optimización móvil continua (Parte 6): La lógica del optimizador automático y su estructura
Describiendo la creación de la optimización móvil automática, al fin hemos llegado a la estructura interna del propio optimizador automático. Este artículo puede resultar útil a aquellos que deseen mejorar el proyecto creado, o bien quieran simplemente analizar la lógica de funcionamiento del programa. En el presente artículo, mostraremos con la ayuda de diagramas UML la estructura interna del proyecto y la interacción de los objetos. Asimismo, analizaremos el proceso de iniciación de las optimizaciones, aunque, por el momento, sin describir el proceso de implementación del optimizador.
Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales Trabajando con las series temporales en la biblioteca DoEasy (Parte 39): Indicadores basados en la biblioteca - Preparación de datos y eventos de la series temporales
En el presente artículo, analizaremos la aplicación de la biblioteca DoEasy para crear indicadores de periodo y símbolo múltiples. Hoy, vamos a preparar las clases de la biblioteca para trabajar con indicadores y poner a prueba la correcta creación de series temporales para su posterior uso como fuentes de datos en los indicadores. Asimismo, organizaremos la creación y el envío de los eventos de series temporales.
Creando un EA gradador multiplataforma: simulación del asesor multidivisa Creando un EA gradador multiplataforma: simulación del asesor multidivisa
En un solo mes, los mercados han caído más de un 30%. ¿Acaso no se trata del mejor momento para simular asesores basados en cuadrículas y martingale? Este artículo es una continuación de la serie de artículos "Creando un EA gradador multiplataforma" cuya publicación, en principio, no estaba planeada. Pero, si el propio mercado nos ofrece la posibilidad de organizar un test de estrés para el asesor gradador, ¿por qué no aprovechar la oportunidad? Pongámonos manos a la obra.
El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL. Parte 2 El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL. Parte 2
En este artículo, presentamos un nuevo concepto para la descripción de la interfaz de ventana de los programas MQL con la ayuda de las construcciones del lenguaje MQL. La creación de GUI basadas en el marcado MQL ofrece una funcionalidad adicional para almacenar la caché y generar de manera dinámica elementos, y también para gestionar los estilos y los nuevos esquemas de procesamiento de eventos. Aquí, ofrecemos la versión mejorada de la biblioteca estándar de los elementos de control.