El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL. Parte 2

15 julio 2020, 10:48
Stanislav Korotky
0
532

En la primera parte de la presente publicación, analizamos los principios descriptivos de la disposición de la interfaz gráfica de los programas MQL en el lenguaje MQL. Para implementarlos, necesitamos crear varias clases que se encarguen directamente de inicializar los elementos de la interfaz, combinar estos y ajustar sus propiedades. Ahora, vamos a pasar a ejemplos más complicados: para luego no distraernos con elementos utilitarios, vamos a prestar un poco de atención a la biblioteca de componentes estándar, que usaremos para construir los ejemplos.

Personalizando la biblioteca de elementos estándar de control

Durante el desarrollo de la interfaz de ventana de los anteriores artículos de OLAP, que también se basaban en la biblioteca estándar y los contenedores CBox, necesitamos introducir ciertas correcciones en los compoenentes de la biblioteca estándar. Como descubrimos, para integrar el sistema de disposición presupuesto, debíamos corregir adicionalmente la biblioteca Controls, tanto ampliando sus posibilidades, como corrigiendo sus errores. Debido a ello, decidimos realizar una copia completa (ramificación en forma de versión) de todas las clases, ubicarlas en la carpeta ControlPlus y trabajar ya solo con ellas.

Estos son los cambios principales.

En prácticamente todas las clases, el nivel de acceso private ha sido cambiado a protected, para posibilitar la expansión de la biblioteca.

Para facilitar la depuración de los proyectos con elementos de GUI, en la clase CWnd se añadido el campo de línea _rtti; asimismo, en el constructor de cada clase derivada, el campo se rellena con el nombre de una clase concreta con la ayuda de la macro RTTI.

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

Esto permite ver en la ventana del depurador la clase real de los objetos nombrados según el enlace de la clase básica (en este caso, el depurador muestra la clase básica).

La información sobre los campos y la alineación del elemento en la clase CWnd está ahora disponible con la ayuda de dos métodos sobrecargados. Además, también es posible cambiar por separado alineación y los campos.

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

El método CWnd::Align ha sido reescrito de acuerdo con el comportamiento esperado de todos los modos de alineación. La implementación estándar no posibilita el desplazamiento hacia el borde del campo establecido, si se ha establecido el estiramiento (ambas dimensiones se someten a ello).

En la clase CWndContainer, hemos añadido el método DeleteAll para eliminar los elementos hijos al eliminar el contenedor. Se llama desde Delete(CWnd *control) si el puntero al "control" transmitido contiene un objeto de contenedor.

En distintos lugares de la clase CWndClient, hemos añadido líneas que regulan la visibilidad de las barras de desplazamiento, que puede cambiar al modificarse las dimensiones.

La clase CAppDialog ahora tiene en cuenta la instance_id de la ventana al asignar los indicadores a los elementos de la interfaz. Sin esta corrección, los elementos de control en ventanas diferentes pero con el mismo nombre provocaban un conflicto (influían unos en otros).

En los grupos de "controles" —CRadioGroup, CCheckGroup, CListView—, el método Redraw se ha hecho virtual, para que las clases herederas adaptables puedan reaccionar correctamente al cambio de tamaño. Además, hemos corregido ligeramente el recálculo de la anchura de sus elementos hijos.

Asimismo, hemos añadido a las clases CDatePicker, CCheckBox y CRadioButton el método virtual OnResize, con el mismo objetivo. En la clase CDatePicker, hemos corregido el error relacionado con la baja prioridad del calendario emergente (las pulsaciones del ratón han pasado a través de él).

El método CEdit::OnClick no permite "pasar" la pulsación del botón.

Además, anteriormente ya habíamos desarrollado varias clases de "controles" que daban soporte al cambio de tamaño, y dentro del marco del presente proyecto, el número de clases adaptables había sido ampliado. Sus archivos se ubican en la carpeta Layouts.

  • ComboBoxResizable
  • SpinEditResizable
  • ListViewResizable
  • CheckGroupResizable
  • RadioGroupResizable

Recordemos que algunos "controles", tales como los botones o el campo de edición, dan soporte al estiramiento desde el principio.

En el diadrama de clases, mostramos la estructura general de la biblioteca de elementos estándar, teniendo en cuenta las versiones adaptadas con soporte de adaptabilidad y contenedores ajenos.

Jerarquía de los elementos de control

Jerarquía de los elementos de control


Generación de los elementos y almacenamiento en caché de los mismos

Hasta ahora, los elementos se han construido como instancias automáticas dentro del objeto de ventana. En esencia, se trata de "minucias" que luego se inician con métodos como Create. El sistema para disponer los elementos de la GUI puede crear esos elementos por sí mismo, sin obtenerlos de la ventana. Para ello, basta con tener un cierto repositorio. Vamos a llamarlo LayoutCache.

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

En esencia, se trata de la matriz de punteros de la clase básica (común para todos los elementos), donde podemos ubicarlos con el método save. Asimismo, en la interfaz se implementan (si es posible en este nivel abstracto) o declaran (para su posterior redefinición) varios métodos para buscar elementos según su número, nombre, enlace o hecho de relaciones de "parentesco" (relación inversa de los elementos incorporados respecto al contenedor).

Vamos a añadir la caché como miembro estático a la clase LayoutBase.

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

Cada ventana deberá crear para sí misma una instancia de la caché y establecerla como caché de trabajo con la ayuda de setCache al inicio de un método del tipo CreateLayout. Dado que los programas MQL son del mismo tipo, tenemos la garantía de que las ventanas (si necesitamos varias) no se formarán paralelamente, ni competirán por el puntero "cacher". Limpiaremos el puntero automáticamente en el destructor de LayoutBase; cuando la pila termine, esto indicará que hemos salido del último contenedor externo en la descripción de la disposición, y no tendremos que guardar nada más.

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

El reseteo del enlace no significa que limpiemos la caché. Simplemente, de esta forma se posibilita que la siguiente disposición (potencial) no añada por error allí los "controles" de otra ventana.

Para rellenar la caché, añadiremos una variedad del método init a LayoutBase, esta vez, sin puntero o enlace a un elemento "ajeno" de la GUI en los parámetros.

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

Gracias a la plantilla, tenemos la posibilidad de escribir T y generar objetos durante la disposición (por defecto, 1 objeto a la vez, pero de forma opcional, podemos generar varios).

Para los elementos de la biblioteca estándar, hemos escrito una implementación concreta de la caché, StdLayoutCache (aquí la mostramos abreviada, podrá encontrar el código completo en los anexos).

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

Debemos notar que el método get busca el "control" o bien por su número ordinal (si el parámetro de entrada es positivo), o bien por su identificador (se transmite con el signo negativo). Aquí, enetendemos por identificador el número único asignado por la biblioteca de componentes estándar para despachar los eventos. En los eventos, se transmite en el parámetro lparam.

En la clase aplicada de la ventana, podemos utilizar directamente esta clase StdLayoutCache o escribir una clase derivada de ella.

En el siguiente ejemplo, veremos cómo el almacenamiento en la caché acorta la descripción de la clase de ventana. Pero, antes de proceder a ello, vamos a analizar algunas posibilidades adicionales que nos descubre la caché. Asimismo, las aplicaremos en ejemplos.

Estilizador

Dado que la caché supone un objeto que procesa los elementos de forma centralizada, con su ayuda resulta cómodo ejecutar muchas otras tareas, aparte de la disposición. Concretamente, podemos aplicar a los elementos las reglas de estilo único (color, fuente, sangrías) de forma unificada. En este caso, además, bastará con ajustar este estilo en un solo sitio, sin escribir las mismas propiedades para cada "control" por separado. Además, la caché puede asumir el procesamiento de errores para el almacenamiento caché de los elementos. En potencia, podemos construir de forma dinámica los elementos, colocar estos en la caché e interactuar con todos ellos. Entonces, no necesitaremos en absoluto declarar ningún elemento "explícito". Un poco más tarde, podremos ver en qué se diferencian positivamente los elementos creados de forma dinámica respecto a los automáticos.

Para dar soporte a los estilos centralizados, en la clase StdLayoutCache se ha previsto un método stub:

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

Si usted no quiere utilizar los estilos, no tendrá que copiar nada adicional. No obstante, si usted comprende las ventajas que ofrece la centralización del control de estilos, podrá implementar la clase heredera LayoutStyleable. La interfaz es muy sencilla.

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

El método apply se llamará para cada "control" dos veces: en el estadio de inicialización (STYLE_PHASE_BEFORE_INIT) y en el estadio de registro en el contenedor (STYLE_PHASE_AFTER_INIT). Así, añadimos en los métodos LayoutBase::init la llamada en el primer estadio:

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

y en el destructor, las línea similares, pero con STYLE_PHASE_AFTER_INIT para el segundo estadio.

Necesitamos dos fases, porque existen diferentes objetivos para la estilización. Para algunos elementos, a veces necesitamos establecer propiedades individuales que tengan prioridad sobre las que hemos establecido en el estilizador. En el estadio de inicialización, el "control" todavía está vacío, sin los ajustes realizados en la disposición. En el estadio de registro, en él ya se han establecido todas las propiedades, y sobre su base ya se puede cambiar el estilo. El ejemplo más obvio sería el siguiente. Es mejor representar todos los campos de edición con la bandera "solo lectura" en color gris. Pero la propiedad "solo lectura" se asigna a un "control" en el proceso de disposición, después de la inicialización, y por eso el primer estadio no conviene aquí, necesitamos el segundo. Por otra parte, normalmente, no todos los campos de edición tendrán una bandera, y en todos los demás casos, deberemos establecer el color por defecto, antes de que el lenguaje de la disposición realice una personalización selectiva.

Por cierto, podemos aplicar una tecnología similar para la localización centralizada de la interfaz de los programas MQL en diferentes idiomas.

Procesamiento de eventos

La segunda función que resulta lógico asignar a la caché es el procesamiento de eventos. Para ellos, en la clase LayoutCache se ha añadido un método stub (C — parámetro de plantilla de clase):

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

Una vez más, podemos implementarlo en la clase derivada, pero no es obligatorio. Los códigos de los eventos son determinados por una biblioteca concreta.

Para que este método funcione, necesitaremos macrodefiniciones de captación de eventos, semejantes a las que hay en la biblioteca estándar y se escriben en el mapa, por ejemplo, así:

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

Las nuevas macros redirigirán los eventos al objeto de la caché. Aquí tenemos una:

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

Como podemos ver, la búsqueda dentro de la caché se realiza según el identificador que llega a lparam (pero con el signo inverso), después de lo cual, el elemento encontrado es enviado al manejador onEvent que hemos analizado anteriormente. En principio, podemos no buscar el elemento al procesar cada evento, sino guardar el índice del elemento en la caché en el momento del registro en la misma, y después vincular un manejador específico al índice.

El tamaño actual de la caché es el índice con el que se acaba de guardar el nuevo elemento. Podemos guardar el índice de los "controles" necesarios durante la disposición.

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

Aquí, button1index es una variable de tipo entero en la clase de ventana. Debemos utilizarla en otra macro, definida para procesar los elementos según el índice de la caché:

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

De forma adicional, podemos dirigir los eventos no a la caché, sino directamente a los propios elementos. Para ello, el elemento debe implementar en sí la interfaz Notifiable, convertida en plantilla con la clase "control".

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

En el parámetro parent, podemos transmitir cualquier objeto, incluida la ventana de diálogo. Usando Notifiable como base, por ejemplo, podemos crear fácilmente un botón heredero de CButton.

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

Tenemos 2 macros para trabajar con los elementos de "notificación". Su diferencia reside en el número de parámetros: ON_EVENT_LAYOUT_CTRL_ANY permite transmitir como último parámetro un objeto aleatorio, mientras que ON_EVENT_LAYOUT_CTRL_DLG no tiene este parámetro, ya que siempre envía en calidad de objeto el "this" de la ventana de diálogo.

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

Vamos a analizar diferentes variantes de procesamiento de eventos en el contexto del segundo ejemplo.

Ejemplo 2. Ventana de diálogo con elementos de control "Controls"

El proyecto demo contiene la clase CControlsDialog con los tipos básicos de "controles" de la Biblioteca Estándar. Por analogía con el primer parámetro, eliminamos todos los métodos para su creación y los sustituimos por un único CreateLayout. Por cierto, en el antiguo proyecto, había nada menos que 17 de estos métodos, que se llamaban uno tras otro con la ayuda de operadores condicionados de composición compleja.

Para que, al generar los "controles", podamos guardar estos en la caché, vamos a añadir una clase sencilla de caché y, ya de paso, la clase de estilización. Primero, mostramos la caché.

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

En la clase de la caché se declara el manejador de eventos onEvent, que incluiremos mediante un mapa de eventos. Aquí, el manejador envía un mensaje a la ventana padre, donde, al igual que en las anteriores versiones del ejemplo, se muestra el campo informativo.

En la clase del estilizador, hemos previsto el establecimiento de campos iguales en todos los elementos, una fuente no estándar en todos los botones, y también la representación de CEdit con el atributo "solo lectura" en color gris (tenemos uno así, pero si lo añadimos, caerá automáticamente en el ajuste general).

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

El enlace a la caché se guarda en la ventana; aquel se crea y elimina, respectivamente, en el constructor y el destructor. En este caso, además, durante la creación se transmite como parámetro el enlace a la ventana que ofrece la retroalimentación.

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

Ahora, vamos a analizar por orden el método CreateLayout. Como hay descripciones bastante detalladas, podría parecer que el método es muy largo, pero en realidad no es así. Si quitamos los comentarios ilustrativos (que no habrá en el proyecto real), el método se reducirá en una pantalla, y no contendrá una lógica compleja.

Al inicio, la caché se activa llamando a setCache. Después, en el primer bloque, se describe el contenedor principal CControlsDialog. No estará en la caché, porque transmitimos el enlace al "this" ya creado.

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

A continuación, creamos una instancia implícita del contenedor incorporado de la clase CBox para la zona de cliente de la ventana. Su orientación será vertical, así que los contenedores incorporados rellenarán el espacio de abajo hacia arriba. Guardamos el enlace al objeto en la variable m_main, porque tendremos que llamar su método Pack después de modificar el tamaño de la ventana. Si su ventana de diálogo no es adaptable, no tendrá que hacer esto. Finalmente, establecemos campos cero para la zona de cliente, así como la alineación de todos los bordes, para que el panel ocupe toda la ventana incluso después de cambiar el tamaño.

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

En el siguiente nivel, en primer lugar va el contenedor, que ocupará todo el ancho de la ventana, pero con una altura algo superior al campo de edición. Además, con la ayuda de la alineación WND_ALIGN_TOP (aparte de WND_ALIGN_WIDTH), estará "pegado" al borde superior de la ventana.

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

Dentro, se encuentra el único "control" de la clase CEdit en el modo "solo lectura". Para él, hemos reservado la variable explícita m_edit, así que no entrará en la caché.

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

En este momento, tenemos inicializados 3 elementos. Después de cerrar el paréntesis, el objeto de disposición edit será eliminado, y durante la ejecución de su destructor, m_edit será añadido al contenedor "editrow". A continuación, aquí va también un paréntesis de cierre. Este elimina el contexto en el que "ha vivido" el objeto de disposición editRow, y por eso, el contenedor se añade a su vez al contenedor restante en la pila de la zona de cliente. De esta forma, se forma la primera serie de la disposición vertical en m_main.

Después, va una serie de tres botones. Primero, creamos un contenedor para estos.

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

Aquí, debemos prestar atención al método no estándar de alineación WND_ALIGN_CONTENT. Significa lo siguiente.

Añadimos a la clase CBox el algoritmo de escalado de elementos incorporados según el tamaño del contenedor. Se ejecuta en el método AdjustFlexControls, y solo se pone en marcha si en las banderas de alineación del contenedor se indica el valor especial WND_ALIGN_CONTENT. Forma parte de la enumeración no estándar ENUM_WND_ALIGN_FLAGS. El contenedor analiza los "controles" para comprobar cuáles de ellos tienen un tamaño fijo, y cuáles no. Los "controles" con un tamaño fijo son aquellos para los que se indica una alineación en los lados del contenedor (en una dimensión concreta). Para todos estos "controles", el contenedor calcula la suma de sus tamaños, la resta del tamaño total del contenedor y divide de forma proporcional el resto entre todos los "controles" restantes. Por ejemplo, si en el contenedor hay dos "controles", y ninguno de ellos tiene vinculación, dividirán por dos entre sí toda la zona del contenedor.

Se trata de un modo muy cómodo, pero no conviene abusar de él cuando haya multitud de contenedores incorporados unos en otros, porque el algoritmo calcula el tamaño de la alineación de los elementos internos en el área del contenedor en una pasada, y a su vez se adapta al contenido, generando incertidumbre (por ese motivo, en las clases de disposición se ha creado el evento especial ON_LAYOUT_REFRESH, que puede enviarse la ventana a sí mismo, para repetir los cálculos del tamaño).

En el caso de nuestra serie con tres botones, estos cambiarán su longitud de forma proporcional cuando se modifique la anchura de la ventana. El primer botón de la clase CButton se crea de forma implícita y se guarda en la caché.

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

El segundo botón tiene la clase NotifiableButton (ya lo hemos descrito más arriba). El botón procesará los mensajes por sí mismo.

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

El tercer botón se crea usando como base la variable definida explícitamente de la ventana m_button3 y posee la propiedad "tecla especial".

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

Preste atención: todos los botones han sido enmarcados en sus propios bloques de llaves. Por eso, se añaden en una serie en el orden en el que se encuentran los paréntesis de cierre marcados con 1, 2, 3, es decir, en el orden natural. Podríamos no hacer estos bloques "personales" para cada botón y limitarnos al bloque general del contenedor, pero entonces, los botones se añadirían en orden inverso, porque los destructores de objetos siempre se llaman en orden inverso respecto a su creación. Podríamos "dominar" la situación invirtiendo el orden de descripción de los botones en la disposición.

En la tercera serie, se ubica el contenedor con los elementos de control "spinner" y calendario. El contenedor se crea de forma "anónima" y se guarda en la caché.

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

Finalmente, el último contenedor ocupa toda la zona restante de la ventana y contiene las dos columnas con elementos. Los colores llamativos se han asignado exclusivamente para mostrar de forma clara dónde se ubica cada contenedor en la ventana.

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

Aquí, debemos destacar especialmente que las dos columnas m_lists_column1 y m_lists_column2 se rellenan, no en el propio método CreateLayout, sino con la ayuda del método auxiliar createSubList. La llamada de la función, desde el punto de vista de la disposición, no se diferencia en nada de la entrada en el siguiente bloque con paréntesis. Esto significa que la disposición no está obligada a constar de una larga lista estática, sino que puede incluir fragmentos cambiantes en función de la situación. O podemos incluir un mismo fragmento en distintas ventanas de diálogo.

En nuestro ejemplo, modificando el segundo parámetro de la función, podemos lograr que cambie el orden de las columnas en la ventana.

      }
    }

Después de cerrar todos los paréntesis, todos los elementos de la GUI son inicializados y conectados unos a otros. Llamamos al método Pack (directamente, o desde SelfAdjustment, desde donde también se llama en respuesta a la solicitud de la ventana de diálogo adaptable).

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

No vamos a analizar con detalle el método createSubList. Dentro se ha implementado la posibilidad de generar un conjunto de 3 "controles" (cuadro combinado, grupo de opciones y grupo de radio) o una lista (ListView), además, todos en su ejecución adaptable. Lo interesente es que el rellenado de los "controles" se realiza con la ayuda de otra clase de generadores ItemGenerator.

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

El único método de esta clase se llama desde la disposición para el "control" object hasta que el método retorne false (signo de finalización de los datos).

Por defecto, se ofrecen varios generadores sencillos para la biblioteca estándar (usan el método de los "controles" AddItem): StdItemGenerator, StdGroupItemGenerator, SymbolsItemGenerator, ArrayItemGenerator. En concreto, SymbolsItemGenerator permite rellenar un "control" con los símbolos de la "Observación de mercado".

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

En la disposición, se muestra del mismo modo que los generadores de "controles". Como alternativa, hemos decidido transmitir al objeto de disposición, no el enlace al objeto automático o estático del generador (que debía haber sido descrito en algún lugar anterior del código), sino el puntero al generador distribuido dinámicamente.

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

Para ello, se usa el operador <. El generador distribuido dinámicamente será eliminado de forma automática después de finalizar el trabajo.

Para incluir los nuevos eventos, hemos añadido al mapa las macros correspondientes.

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

La macro ON_EVENT_LAYOUT_CTRL_DLG activa la notificación sobre las pulsaciones del ratón para cualquier botón de la clase NotifiableButton (en nuestro caso, hay uno). La macro ON_EVENT_LAYOUT_INDEX envía el mismo evento al botón con el índice indicado en la caché. En esta macro, podríamos no escribir, porque, con la última línea, la macro ON_EVENT_LAYOUT_ARRAY reenviará la pulsación del ratón a cualquier elemento en la caché si coincide su identificador lparam.

En principio, podríamos trasladar todos los elementos a la caché y procesar sus eventos con el nuevo método, pero el método antiguo también funciona, y podemos combinar ambos.

En la siguiente imagen animada, se muestra la reacción a los eventos.

Ventana de diálogo con elementos de control formada con la ayuda del lenguaje de marcado MQL

Ventana de diálogo con elementos de control formada con la ayuda del lenguaje de marcado MQL

Preste atención a que este método de emisión de eventos se puede determinar de forma indirecta según la signatura de la función mostrada en el campo de información. Asimismo, podemos ver que los eventos llegan no solo a los "controles", sino también a los contenedores. Hemos destacado los marcos rojos de los contenedores para la depuración con la ayuda de la macro LAYOUT_BOX_DEBUG.

Ejemplo 3. Disposiciones dinámicas DynamicForm

En este último ejemplo, hemos analizado la forma en la que todos los elementos serán creados dinámicamente en la caché. Esto nos ofrecerá un par de posibilidades nuevas.

Al igual que en el ejemplo anterior, la caché dará soporte a la estilización de elementos. El único ajuste del estilo serán los mismos campos perceptibles, que permitirán ver la incorporación de los contenedores unos en otros, y también destacar cualquiera de ellos utilizando el ratón.

Dentro del método CreateLayout, se describe la siguiente estructura de interfaz sencilla. El contenedor principal, como siempre, ocupa toda la zona de cliente de la ventana. En la parte superior, se ubica un bloque con dos botones: Inject y Export. Todo el espacio para él lo ocupa un contenedor dividido en dos columnas: izquierda y derecha. La columna izquierda, que hemos marcado en color gris, está inicialmente vacía. En la columna derecha, se encuentra un grupo de botones de radio que permite seleccionar el tipo de elemento de control.

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

Se presupone que, tras seleccionar el tipo de elemento en el grupo de radio, el usuario debe pulsar el botón Inject, y el "control" correspondiente será creado en la parte izquierda de la ventana. Claro que podemos crear secuencialmente varios "controles" diferentes, orientados automáticamente según los ajustes del contenedor. Para implementar esta lógica, el botón Inject tiene la clase NotifiableButton con el manejador onEvent.

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

El contenedor donde deberán colocarse los nuevos elementos primero se busca en la caché según el nombre "column1". Este contenedor va como primer parámetro al crear el objeto injectionPanel. El hecho de que el objeto transmitido se encuentre ya en la caché ha sido especialmente considerado en el algoritmo de disposición, por lo que no será nuevamente añadido, aunque sí que se colocará como siempre en la pila de contenedores. De esta forma, se ofrece la posibilidad de añadir elementos a los contenedores "antiguos".

Dependiendo de la selección del usuario, el objeto del tipo necesario es creado con la ayuda del operador new en el método auxiliar getPtr. Para que los "controles" añadidos funcionen con normalidad, se crearán para ellos identificadores únicos de forma aleatoria. La clase especial AutoPtr se encargará de eliminar el puntero cuando salgamos de un bloque de código.

Si añadimos demasiados elementos, estos se saldrán de los límites del contenedor. Esto sucede porque las clases disponibles de los contenedores no saben reaccionar aún correctamente al verse desbordadas. En este caso, podríamos, por ejemplo, mostrar la barra de desplazamiento, ocultando los elementos que sobresalieran de los bordes,

pero esto no es tan importante. La esencia del ejemplo consiste en que podemos generar un contenido dinámico ajustando la forma, y también posibilitar que los contenedores tengan el relleno y el tamaño necesarios.

Aparte de añadir elementos, esta ventana de diálogo sabe eliminarlos. Cualquier elemento en la forma puede ser destacado con un clic del ratón. En este caso, además, la clase y el nombre se muestran en el log, mientras que el propio elemento es destacado con un marco rojo. Si clicamos sobre un elemento ya destacado, la ventana de diálogo mostrará una solicitud para confirmar la eliminación, a la cual procederá si aceptamos. Todo esto se implementa en nuestra clase de caché.

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

Podemos eliminar cualquier elemento de la interfaz ubicado en la caché, es decir, no solo aquellos que han sido añadidos con el botón Inject. De esta forma, podemos, por ejemplo, eliminar toda la mitad izquierda o la "caja de radio" derecha. No obstante, lo más interesante sucederá cuando intentemos eliminar el contenedor superior con dos botones. Como resultado de ello, el botón Export perderá su vinculación con la ventana de diálogo y permanecerá en el gráfico.

Forma editada: añadiendo y eliminando elementos

Forma editada: añadiendo y eliminando elementos

Esto sucede así porque se trata del único elemento escrito a propósito como variable automática, no como dinámica (en la clase de la forma existe la instancia CButton m_button3).

Cuando la biblioteca estándar intenta eliminar los elementos de la interfaz, delega esta tarea en la clase de matrices CArrayObj, y esta, a su vez, comprueba el tipo de puntero y elimina solo los objetos con POINTER_DYNAMIC. Así, resulta obvio que, para construir una interfaz adaptativa donde los elementos puedan sustituir unos a otros, o bien sean totalmente eliminados, resultará deseable utilizar una ubicación dinámica, y la caché ofrece para ello soluciones ya preparadas.

Finalmente, vamos a recurrir al segundo botón de la ventana de diálogo: Export. Como podemos suponer por su nombre, está pensado para guardar el estado actual de la ventana de diálogo como un archivo de texto en la sintaxis analizada de las disposiciones MQL. Claro que la forma permite ajustar su aspecto de manera limitada, con fines ilustrativos, pero la propia posibilidad de descaragar el aspecto externo en un archivo MQL preparado, que después podremos copiar fácilmente en un programa, obteniendo la misma interfaz, supone en potencia una tecnología bastante valiosa. Obviamente, se traslada solo la interfaz, mientras que el código de procesamiento de eventos o los ajustes generales del estilizador se deben incluir de forma independiente.

La exportación se realiza con la ayuda de la clase LayoutExporter, que no analizaremos con detalle: los códigos fuente se adjuntan al artículo.

Conclusión

En el presente artículo, hemos comprobado la viabilidad del concepto descriptivo de la disposición de la interfaz gráfica de los programas MQL en el lenguaje MQL. El uso de la generación dinámica de elementos con almacenamiento centralizado en la caché nos permite crear y controlar la jerarquía de los componentes. Usando la caché como base, podemos implementar la mayoría de las tareas relacionadas con el proyecto de una interfaz, en concreto, el cambio homogéneo del estilo, el procesamiento de eventos, la edición de la disposición sobre la marcha y su guardado en un formato adecuado para su posterior uso.

Si sumamos todas estas funciones, resultará que disponemos de prácticamente todo lo necesario para construir un editor visual de formas. Este podría dar soporte solo a las propiedades más importantes, comunes para muchos "controles", pero permitiendo formar plantillas de interfaz. Sin embargo, vemos que incluso la etapa inicial de valoración del nuevo concepto ha requirido muchos esfuerzos. Por eso, la implementación de un editor completo, supondrá en la práctica una tarea bastante complicada. Y esto ya es otra historia.

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

Archivos adjuntos |
MQL5GUI2.zip (98.72 KB)
Trabajando con las series temporales en la biblioteca DoEasy (Parte 38): Colección de series temporales - Actualización en tiempo real y acceso a los datos desde el programa Trabajando con las series temporales en la biblioteca DoEasy (Parte 38): Colección de series temporales - Actualización en tiempo real y acceso a los datos desde el programa

En el artículo, analizaremos la actualización en tiempo real de los datos de las series temporales, así como el envío de mensajes sobre el evento "Nueva barra" al gráfico del programa de control de todas las series temporales de todos los símbolos para poder procesar estos eventos en nuestros propgramas. Para determinar la necesidad de actualizar las series temporales para el símbolo y los periodos del gráfico no actuales, usaremos la clase "Nuevo tick".

Trabajando con las series temporales en la biblioteca DoEasy (Parte 37): Colección de series temporales - Base de datos de series temporales según el símbolo y el periodo Trabajando con las series temporales en la biblioteca DoEasy (Parte 37): Colección de series temporales - Base de datos de series temporales según el símbolo y el periodo

El artículo está dedicado a la creación de la colección de series temporales de los marcos temporales establecidos para todos los símbolos utilizados en el programa. Vamos a crear la colección de series temporales, y también los métodos para establecer los parámetros de las series temporales contenidas en la colección. Asimismo, rellenaremos por primera vez con datos históricos las series temporales creadas en la colección.

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.

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

En este artículo, finalizaremos la descripción del nuevo concepto para la construcción de la interfaz de ventana de los programas MQL con la ayuda de las construcciones del lenguaje MQL. El editor gráfico especial permitirá ajustar de forma interactiva una disposición formada por las clases básicas de elementos de GUI, y después exportarla a una descripción MQL para usarla en nuestro proyecto MQL. Asimismo, presentamos la construcción interna del editor y las instrucciones para el usuario. Los códigos fuente se adjuntan al final del artículo.