English Русский 中文 Deutsch 日本語 Português
El lenguaje MQL como medio de marcado de la interfaz gráfica de programas MQL. Parte 1

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

MetaTrader 5Ejemplos | 22 junio 2020, 13:38
1 446 0
Stanislav Korotky
Stanislav Korotky

¿Necesitamos una interfaz gráfica de ventana en los programas de MQL? La opinión general sobre este tema es que por una parte, los tráders sueñan con el método más sencillo de comuniciación con el robot comercial: un botón de permiso para comerciar que comience mágicamente a "ganar pasta"; por otra, y por eso mismo se trata de un sueño, este método se aleja mucho de la realidad, dado que, normalmente, necesitamos seleccionar laboriosa y exhaustivamente un montón de ajustes antes de que el sistema funcione, y además, después de ello, tenemos que controlar el proceso y corregirlo manualmente en caso necesario. Y eso por no mencionar los adeptos del comercio manual: en su caso, la selección de un oanlel comercial cómodo e intuitivo supone la mitad de su éxito. En general, podemos constatar que la interfaz de ventana, de una u otra forma, resulta más necesaria que inútil.

Introducción a la tecnología de marcado con GUI

Para construir la interfaz gráfica, MetaTrader ofrece varios de los elementos de control más demandados, tanto en forma de objetos independientes susceptibles de colocarse en el gráfico, como envueltos por los "controles" de la biblioteca estándar que se pueden organizar en una única ventana interactiva. Asimismo, existen varias soluciones alternativas en cuanto a la construcción de GUI. Sin embargo, en todas estas bibliotecas, rara vez se toca la cuestión de la distribución de los elementos, digamos, de la automatización del diseño de la interfaz.

Claro que son pocos los que piensan en dibujar en el gráfico una ventana no menos compleja que el propio MetaTrader, pero un panel comercial simple a primera vista puede constar de decenas de "controles" cuya gestión desde MQL se convierte en una auténtica rutina.

La disposición (layout) es un método para describir de forma unificada la colocación y los atributos de los elementos de una interfaz, que posibilita la creación de ventanas y su conexión con el código de gestión.

Vamos a recordar cómo se crea una interfaz en los ejemplos estándar de MQL.

  bool CPanelDialog::Create(const long chart, const string name, const int subwin, const int x1, const int y1, const int x2, const int y2)
  {
    if(!CAppDialog::Create(chart, name, subwin, x1, y1, x2, y2)) return(false);
    // create dependent controls
    if(!CreateEdit()) return(false);
    if(!CreateButton1()) return(false);
    if(!CreateButton2()) return(false);
    if(!CreateButton3()) return(false);
    ...
    if(!CreateListView()) return(false);
    return(true);
  }
  
  bool CPanelDialog::CreateButton2(void)
  {
    // coordinates
    int x1 = ClientAreaWidth() - (INDENT_RIGHT + BUTTON_WIDTH);
    int y1 = INDENT_TOP + BUTTON_HEIGHT + CONTROLS_GAP_Y;
    int x2 = x1 + BUTTON_WIDTH;
    int y2 = y1 + BUTTON_HEIGHT;
  
    if(!m_button2.Create(m_chart_id, m_name + "Button2", m_subwin, x1, y1, x2, y2)) return(false);
    if(!m_button2.Text("Button2")) return(false);
    if(!Add(m_button2)) return(false);
    m_button2.Alignment(WND_ALIGN_RIGHT, 0, 0, INDENT_RIGHT, 0);
    return(true);
  }
  ...

Todo se hace en un estilo imperativo, con la ayuda de llamadas del mismo tipo. El código de MQL resulta largo y poco efectivo desde el punto de vista de su repetición para cada elemento, además, en cada caso se utilizan constantes propias (las llamadas "cifras mágicas", que se consideran fuentes potenciales de error). La escritura de semejante código es una ocupación ingrata (concretamente, los errores del tipo Copy&Paste, una parábola de dominio público entre los desarrolladores), y en el caso de que sea necesario insertar un nuevo elemento y mover los antiguos, seguramente tendremos que recalcular y modificar manualmente la mayor parte de las "cifras mágicas".

Aquí tenemos el aspecto habitual de la descripción de los elementos de interfaz en la clase de la ventana de diálogo.

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

Esta lista plana de "controles" puede resultar muy larga, y resulta difícil de percibir y sostener sin las "pistas" claras que pueda ofrecer la disposición.

En otros lenguajes de programación, el diseño de la interfaz está normalmente separado de la programación. Para describir la disposición de los elementos, se usan lenguajes declarativos, por ejemplo, XML o JSON.

Concretamente, podrá familiarizarse con los principios de descripción de los elementos de la interfaz para los proyectos en la documentación o en los manuales. Basta con tener una idea general de XML para captar el concepto. En estos archivos se ve claramente la jerarquía, se definen los elementos-contenedores (LinearLayout, RelativeLayout) y los "controles" individuales (ImageView, TextView, CheckBox); en las porpiedades se establece el ajuste automático de las dimensiones según el contenido (match_parent, wrap_content) y los enlaces a la descripción centralizada de estilos; también se indican opcionalmente los manejadores de eventos, aunque todos los elementos, claro está, se pueden ajustar adicionalmente y vincular a estos otros manejadores desde el código ejecutable.

Si recordamos la plataforma .Net, en ella también se utiliza una descripción declarativa de interfaces con la ayuda de XAML. Para aquellos que nunca han programado en C# y otros lenguajes con infraestructura de código gestionado (cuyo concepto, a propósito, es muy semejante a la plataforma MetaTrader y su lenguaje MQL "gestionado"), aquí también resultan obvios los principales momentos a considerar: los "controles", los contenedores, las propiedades y la reacción a las acciones de un usuario en un mismo saco.

¿Para qué separan la disposición del código y la describen en un lenguaje especial? Estas son las principales ventajas de este enfoque.

  • la clara representación de las relaciones jerárquicas entre elementos y contenedores;
  • la agrupación lógica;
  • el establecimiento unificado de la ubicación y la alineación;
  • el sencillo registro de las propiedades y sus valores;
  • las declaraciones permiten implementar la generación automática de un código que soporte el ciclo vital y la gestión de elementos (creación, ajuste, interacción activa, eliminación);
  • el nivel general de abstracción (propiedades generales, estados, fases de inicialización y procesamiento), que permite desarrollar GUI independientemente de la codificación;
  • el uso repetido (múltiple) de disposiciones (el mismo fragmento puede incluirse varias veces en distintas ventanas de diálogo);
  • la implemnetación/generación dinámica de contenido sobre la marcha (por analogía con la alternancia de varias pestañas, con su propio conjunto de elementos a aplicar individualmente para cada una);
  • la creación dinámica de "controles" dentro de la disposición, con su correspondiente guardado en una única matriz de punteros a la clase básica (al igual que CWnd, en el caso de la biblioteca estándar MQL);
  • el uso de un editor gráfico aparte para el diseño interactivo de la interfaz: en este caso, el formato especial de descripción de disposiciones ejerce como eslabón que conecta la representación del programa y su parte ejecutiva en el lenguaje de programación;

Para el entorno MQL, se ha intentado muchas veces solucionar ciertas de estas tareas. En concreto, el constructor visual de ventanas de diálogo se presenta en el artículo MQL para "Dummies": Cómo Diseñar y Construir Clases de Objetos, y funciona usando como base la biblioteca MasterWindows. Pero los métodos de disposición y la lista de tipos de elementos soportados se ven fuertemente limitados en él.

Podrá encontrar un sistema de disposición más avanzado, pero sin diseñador visual, en los artículos Aplicación de los contenedores para componer la interfaz gráfica: clase CBox y clase CGrid. Esta da soporte a todos los elementos estándar de gestión y de otro tipo heredados de CWndObj o CWndContainer, sin embargo, sigue dejando al usuario la programación rutinaria de creación y ubicación de los componentes.

Desde un punto de vista conceptual, este enfoque con los contenedores es muy tecnológico (basta con indicar su popularidad en casi todos los lenguajes de marcado), y por eso vamos a utilizarlo. En uno de nuestros anteriores artículos (Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales), propusimos una modificación de los contenedores CBox y CGrid, así como algunos elementos de gestión para dar soporte de las propiedades de "elasticidad". A continuación, vamos a aprovechar este tiempo invertido y mejorarlos para solucionar las tareas de ubicación automática de elementos usando como ejemplo los objetos de la biblioteca estándar.

Editor gráfico de interfaz: a favor y en contra

La principal función del editor gráfico de interfaz consistirá en crear y ajustar sobre la marcha las propiedades de los elementos en una ventana, según los comandos del usuario. Esto presupone la presencia de varios campos de edición para la selección de propiedades, y para que estos funcionen, necesitaremos conocer la lista de propiedades y sus tipos para cada clase. De esta forma, cada "control" deberá tener dos versiones mutuamente enlazadas: las llamadas run-time (para el trabajo estándar) y design-time (para proyectar la interfaz de forma interactiva). La primera versión de los "controles" ya la tenemos inicialmente: se trata de la clase que funciona en las ventanas. La segunda versión es un envoltorio del "control", pensado para visualizar y modificar sus propiedades disponibles. Escribir un envoltorio semejante para cada tipo de elementos resultaría demasiado duro. Por eso, es preferible automatizar este proceso. En teoría, para estos objetivos se puede utilizar el analizador de sintaxis de MQL descrito en el artículo Análisis sintáctico de MQL usando las herramientas de MQL. En muchos lenguajes de programación, el concepto de propiedad (property) se ha sacado a la sintaxis y combina en sí el "setter" y el "getter" de un cierto campo interno del objeto. En MQL, este fenómeno no se ha dado todavía, pero en las clases de ventana de la bibloteca estándar se aplica un principio semejante: para establacer y leer un mismo campo, se usa una pareja de métodos homónimos "invertidos", de los cuales uno adopta un valor de tipo concreto, mientras que el otro lo retorna. Por ejemplo, así se ha definido la propiedad "solo lectura" del campo de edición CEdit:

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

Y así se posibilita el trabajo con el límite superior CSpinEdit:

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

Con la ayuda del parser MQL, podemos encontrar estas parejas de métodos en cada clase y después formar su lista general teniendo en cuenta la jerarquía de herencia, generando acto seguido la clase-envoltorio para establecer y leer de forma interactiva las propiedades encontradas. Solo tenemos que hacer esto una vez para cada clase de "controles" (con la condición de que la clase no cambie sus propiedades públicas).

El proyecto, aunque se puede implementar, tiene unas dimensiones considerables. Antes de proceder a su creación, merece la pena ponderar todas las ventajas y desventajas.

Destacaremos 2 objetivos principales en el diseño: el establecimiento de la subordinación jerárquica de los elementos y sus propiedades. Si se encontraran métodos alternativos para alcanzarlas, podríamos renunciar al redactor visual.

Usando el sentido común, comprendemos que las propiedades principales de todos los elementos son estándar: tipo, tamaño, alineación, texto, estilo (color). También es posible establecer las propiedades específicas en el código MQL, ya que son operaciones individuales y, normalmente, relacionadas con la lógoca comercial. En cuanto al tipo, el tamaño y la alineación, estos se establecen de forma implícita con la ayuda de la propia jerarquía de los objetos.

De esta forma, llegamos a la conclusión de que, en la mayoría de casos, en lugar del editor completo, basta con disponer de un cómodo método de descripción de la jerarquía de los elementos de la interfaz.

Imaginemos los elementos de control y los contenedores en la clase de la ventana de diálogo no como una lista completa, sino con una sangría que imita la anidación (subordinación) enforma de árbol.

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

Así, la estructura de la ventana de diálogo es bastante mejor, pero el cambio de formateo, claro está, no influirá en forma alguna en la capacidad del programa de interpretar estos objetos de una forma especial.

De forma ideal, querríamos tener una manera de describir la interfaz, sobre cuya base los elementos de control se crearan por sí mismos de acuerdo con la jerarquía establecida, encontrando el lugar correcto en la pantalla y calculando el tamaño adecuado.

Proyectando el lenguaje del marcado

Bien, tenemos que desarrollar un lenguaje de marcado que describa la estructura general de la interfaz de ventana y las propiedades de sus elementos aparte. Aquí, podríamos apoyarnos en un formato ampliamente extendido, como XML, y reservar un conjunto de tags existentes. Incluso podríamos tomarlos prestados de algún otro entorno de desarrollo, como aquellos que hemos mencionado más arriba. Pero entonces, deberíamos parsear XML e integrarlo después en MQL, poniendo también en acción para la creación los ajustes de los objetos. Además, dado que ya no necesitamos el editor visual, el lenguaje "externo" de marcado como medio de comunicación entre el editor y el entorno tampoco es ya necesario.

En estas condiciones, surge la idea: ¿y no será posible usar el propio MQL como lenguaje de marcado? Realmente, sí que se puede.

La jerarquía está incorporada en el lenguaje MQL inicialmente. Aquí nos vienen de inmediato a la cabeza clases heredadas una de otra. Pero las clases describen la estadística jerárquica formada hasta la ejecución del código. Y nosotros necesitamos una jerarquía que se pueda interpretar a medida que se ejecuta el código MQL. En algunos otros lenguajes de programación, para estos objetivos (el análisis de la jerarquía y la estructura interna de las clases del propio programa) se dispone de un recurso incorporado, la llamada información sobre los tipos de tiempo de ejecución (run-time type information, RTTI, conocida como "reflejo" (reflections)). Pero en MQL, no existen esos medios.

Sin embargo, en MQL también existe otra jerarquía (como en la mayoría de lenguajes de programación): la jerarquía de contextos de ejecución de fragmentos de código. Cada pareja de llaves en una función/método (es decir, con la excepción de aquellas llaves que se usan para describir las clases y estructuras) forma un contexto: la zona vital de las variables locales. Dado que el nivel de anidación de los bloques no está limitado, podemos describir con su ayuda jerarquías aleatorias.

Ya se utilizó un enfoque semejante en MQL, en concreto, para implementar un perfilador propio, un medidor de la velocidad de ejecución del código (hay un artículo en el blog inglés MQL's OOP notes: Self-made profiler on static and automatic objects). Su esencia de funcionamiento es sencilla. Si en un bloque de código, aparte de las operaciones que ejecutan una tarea aplicada, declaramos una variable local:

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

esta se creará justo después de la entrada en el bloque, y será eliminada antes de salir del mismo. Esto se refiere a los objetos de cualquier clase, incluyendo aquellos que puedan tener en cuenta este comportamiento. Concretamente, en el constructor y el destructor podemos detectar la hora de estas instrucciones y calcular así la duración del algoritmo aplicado. Claro está que, para acumular estas mediciones, necesitaremos otro objeto mayor: el propio perfilador. Sin embargo, el dispositivo de intercambio de datos entre ellos no es tan importante en estos momentos (si el lector lo desea, odrá encontrar información en el blog). La clave consiste en aplicar el mismo principio para describir la disposición. En otras palabras, tendrá el aspecto siguiente:

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

A medida que se ejecuta este código, se crearán objetos de una cierta clase (que se ha llamado de manera condicional "container"), con un parámetros de plantilla que determina la clase de un elemento concreto de la GUI que se debe generar en la ventana de diálogo. Todos los objetos-contenedores se ubican en una matriz especial en modo de pila: cada nuevo nivel de anidación añade un contenedor a la matriz; en este caso, además, el bloque actual del contexto está disponible en la cima de la pila, mientras que en su parte más baja (el primer número) siempre va la ventana. En el momento del cierre de cada bloque, todos los elementos hijos creados en él se vincularán automáticamente al padre inmediato (que precisamente se encuentra en la cima de la pila).

Toda esta "magia" debe proporcionar el dispositivo interno de las clases "container" y "control". En realidad, se tratará d ela misma clase "layout", pero, para mayor claridad, en el esquema mostrado más arriba se ha subrayado la diferencia entre los contenedores y los elementos de control. En la práctica, la diferencia reside solo en qué clases son indicadas por los parámetros de la plantilla. Así, las clases Dialog, classA, classB, classC en el ejemplo de arriba, deberían ser contenedores de ventana, es decir, dar soporte dentro de sí a los "controles".

Debemos diferenciar los objetos de disposición auxiliares con un ciclo vital corto (llamados más arriba con nombres como main, top_level, next_level_1, ctrl1, ctrl2, next_level2, ctrl3, ctrl4) y los objetos de las clases de interfaz controlados por ellos (object 1 ... object 8), que permanecerán vinculados tanto unos a otros, como a la ventana. Este código al completo se ejecutará como un método de ventana de diálogo (análogo al método Create), y por eso, el objeto de ventana de diálogo estará disponible como this.

A algunos objetos de disposición les transmitimos los objetos de GUI como variables de clase (object 4, 5, 7, 8), mientras que a otros no (indicamos el nombre y la propiedad). En cualquier caso, el objeto de GUI debe existir, pero no siempre nos es necesario en su forma explícita. Si un "control" se usa para la posterior interacción con un algoritmo, resultará cómodo tener un enlace a él. Los contenedores, entre tanto, no están vinculados con la lógica del programa, y ejecutan solo las funciones de ubicación de los "controles", por eso, su creación tiene lugar de forma implícita dentro del sistema de disposición.

La sintaxis concreta del registro de propiedades y su enumeración las desarrollaremos un poco más tarde.

Las clases para la interfaz de disposición: el nivel abstracto

Vamos a proceder al desarrollo de las clases que nos permitirán implementar la formación de la jerarquía de los elementos de interfaz. Potencialmente, este enfoque debe ser aplicable a cualquier biblioteca de "controles", por eso, hemos dividido el conjunto de clases en 2 partes: las clases abastractas (con funcionalidad general) y las aplicadas, relacionadas con las particularidades de una biblioteca concreta de elementos de control estándar (las clases herederas CWnd). Comprobaremos la funcionalidad del concepto precisamente con las ventanas de diálogo estándar, pero si el lector lo desea, podrá aplicarla a otras bibliotecas, guiándose por una capa abstracta.

La clase LayoutData ocupará el lugar central.

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

En ella se guarda la cantidad mínima de información inherente a cualquier elemento de disposición: el nombre único _id y las coordenadas. Vamos a aclarar que este campo _id se determina a un nivel abstracto, y que en cada biblioteca de GUI concreta puede "representarse" en su propiedad de "controles". En concreto, en la biblioteca estándar, este campo se llama m_name y está disponible a través del método público CWnd::Name. El nombre de dos objetos no puede coincidir. En CWnd, también se determina el campo m_id del tipo long: este se usa para despachar los mensajes. Cuando lleguemos a la implementación aplicada, no deberemos confundirlo con nuestra _id.

Además, solo la clase LayoutData ofrece un repositorio estático de ejemplares propios en forma de pila (stack) y un identificador del ejemplar de la ventana (rootId). La estaticidad de los dos últimos miembros no es un problema, porque cada programa MQL se ejecuta en un único flujo, e incluso si en ella hay varias ventanas, en cada momento temporal solo puede crearse una de ellas. Cuando una ventana haya sido dibujada, la pila se vaciará y estará lista para trabajar con otra. El identificador de ventana rootId es conocido para la biblioteca estándar como el campo m_instance_id en la clase CAppDialog. Para las otras bibliotecas, deberá existir algo semejante (no una línea necesariamente, sino algo único que se pueda convertir en línea), porque, de lo contrario, la ventana podría entrar en conflicto consigo misma. Hablaremos de este problema más tarde.

El heredero de la clase LayoutData será una LayoutBase tipificada. Se trata de un modelo de la misma clase de disposición que ha generado los elementos de la interfaz según el código MQL con bloques de llaves en forma de instrucciones.

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

Sus dos parámetros de plantilla P y C se corresponden con las clases de los elementos que sirven de contenedores y "controles".

Por definición, los contenedores contienen "controles" y/o otros contenedores, al tiempo que los "controles" se perciben como una unidad, y no pueden contener nada. Aquí, debemos notar especialmente que por "control" entendemos una unidad de interfaz lógicamente indivisible, que dentro puede constar realmente de multitud de objetos auxiliares. En concreto, las clases CListView o CComboBox de la biblioteca estándar son "controles", pero dentro han sido implementadas con ayuda de varios objetos. Estos son ya detalles de la implementación: en otras bibliotecas, semejantes tipos de elementos de control se pueden implementar como un lienzo en el que se dibujan botones y texto. En el contexto de las clases de disposición abstractas, no debemos profundizar en ello, pues violaríamos los principios de encapsulamiento, pero en la implementación aplicada, diseñada para una biblioteca concreta, está claro que deberemos tener en cuenta este matiz (y distinguir los contenedores auténticos de los "controles" de composición compleja).

Para la biblioteca estándar, los mejores candidatos a parámetros de plantilla P y C son CWndContainer y CWnd. Adelantándonos un poco, notaremos que no es posible usar CWndObj como la clase para los "controles", porque muchos "controles" se heredan de CWndContainer. Entre ellos se cuentan, por ejemplo, CComboBox, CListView, CSpinEdit, CDatePicker y otros. Sin embargo, como parámetro C, deberemos seleccionar la clase general más próxima de todos los "controles", que, en el caso de la biblioteca estándar, será CWnd. Como podemos ver, la clase de contenedores (como CWndContainer) puede "cruzarse" en la práctica con elementos sencillos, y por eso, en lo sucesivo, deberemos lograr que se compruebe de forma más precisa si un ejemplar concreto es un contenedor o no. De la misma forma, como parámetro P, deberemos seleccionar la clase general más próxima de todos los contenedores. En la biblioteca estándar, la clase de eventana es CDialog, heredera de CWndContainer, pero, aparte de ella, vamos a utilizar las clases de la rama CBox para agrupar los elementos dentro de la ventanas de diálogo, y esta procede de CWndClient, que a su vez viene de CWndContainer. De esta forma, el heredero común más próximo es CWndContainer.

Los campos de la clase LayoutBase guardarán los punteros al elemento de interfaz generado por el objeto de disposición.

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

Aquí, container y object indican lo mismo, sin embargo, container no es igual a NULL, solo si el elemento es verdaderamente un contenedor.

La matriz array permite crear con la ayuda de un objeto de disposición un grupo de elementos del mismo tipo, por ejemplo, botones. En este caso, los punteros container y object serán igual a NULL. Para todos los miembros hay disponibles métodos-"getters" triviales, no vamos a mostrarlos todos aquí. Por ejemplo, el enlace a object se puede obtener fácilmente con la ayuda de get().

Los siguientes tres métodos declaran las operaciones abstractas sobre el elemento vinculado; el objeto de disposición debe saber ejecutar dichas operaciones.

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

El método setContainer permite distinguir un contenedor de un "control" normal en el parámetro transmitido. Precisamente en este método se presupone el rellenado del campo container, y si no es NULL, se retorna true.

El método create inicializa el elemento (en la biblioteca estándar, existe un método Create similar en todas las clases, pero, por lo que podemos decir de las demás bibliotecas, por ejemplo, en EasyAndFastGUI, se han implementado métodos análogos; bien es cierto que, en el caso de EasyAndFastGUI, por algún motivo se llaman de forma distinta en clases distintas, por eso, quien quiera incluir en ella el mecanismo de disposición descrito tendrá que escribir clases-adaptadores que unifiquen la interfaz programática de los "controles" de distinto tipo. No obstante, eso no es todo, resulta aún más importante escribir para EasyAndFastGUI clases análogas a CBox y CGrid). Podemos transmitir al método el identificador de elemento deseado, pero no es seguro que el algoritmo ejecutor tenga en cuenta ese deseo de forma completa o parcial (en concreto, puede añadirse instance_id); por eso, podremos conocer el identificador real a partir de la línea retornada.

El método add añade un elemento al elemento-contenedor padre (en la biblioteca estándar, esta operación es ejecutada por el método Add, en EasyAndFastGUI, por lo visto, por MainPointer).

Ahora, vamos a ver cómo se implican estos 3 métodos a nivel abstracto. Cada elemento de la interfaz está vinculado a un objeto de disposición, y experimenta 2 fases: la creación (al inicializar la variable local en el bloque de código) y la eliminación (al salir del bloque de código y llamar al destructor de la variable local). Para la primera fase, escribiremos el método init, que se llamará desde los constructores de las clases herederas.

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

El primer parámetro es el puntero al elemento de la clase correspondiente. Aquí, por ahora vamos a limitarnos a analizar el caso en el que un elemento se suministra desde el exterior. Pero, en el bosquejo de la supuesta sintaxis de la disposición de más arriba, también teníamos elementos implícitos (para estos, solo se indicaba el nombre). Volveremos a este esquema de trabajo un poco más tarde.

El método contiene un puntero al elemento object, comprueba con la ayuda de setContainer si es un contenedor (presuponiendo que, si es así, el campo container también se rellenará), y toma las coordenadas indicadas de los parámetros de entrada o, de forma opcional, del contenedor padre, si ya está en la pila. La llamada de create inicializa el elemento de interfaz. Si la pila aún está vacía, guardamos el identificador en rootId (en el caso de la biblioteca estándar, será instance_id), dado que el primer elemento en la pila siempre será el contenedor principal(en la biblioteca estándar, la clase CDialog o una derivada). Finalmente, si el elemento actual es un contenedor, lo ubicamos en la pila (stack << &this).

El método init es un método de plantilla. Esto permite generar automáticamente los nombres de los "controles" por tipos, pero, además, añadiremos muy pronto otros métodos init análogos. Uno de ellos generará los elementos desde dentro, sin tomarlos preparados desde el exterior, y, en este caso, es necesario un tipo completo. La otra variante de init ha sido pensada para registrar en la disposición varios elementos del mismo tipo a la vez (recordemos el miembro array[]), mientras que las matrices se transmiten por enlaces, y los enlaces no dan soporte a la conversión de tipos ("parameter conversion not allowed", "no one of the overloads can be applied to the function call", dependiendo de la estructura del código), en relación con lo cual, de nuevo será necesario indicar un tipo concreto a través del parámetro de plantilla. De esta forma, todos los métodos init tendrán el mismo contrato de "plantilla" (normas de uso).

Los más interesante tiene lugar en el destructor LayoutBase.

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

Si el elemento actual vinculado es un contenedor, lo eliminamos de la pila, dado que salimos del bloque de llaves correspondiente (el contenedor se ha terminado). El asunto es que, dentro de cada bloque, es precisamente la cima de la pila la que contiene el contenedor con la mayor anidación, donde se añaden (en realidad, ya se han añadido) los elementos encontrados dentro del bloque (pueden ser tanto "controles", como otros contenedores más pequeños). A continuación, el elemento actual se añade con la ayuda del método add al contenedor, que a su vez ha resultado en la cima de la pila.

Las clases para la disposición de interfaz: el nivel aplicado para los elementos de la biblioteca estándar

Vamos a pasar a cosas más concretas: la implementación de las clases para la disposición de los elementos de interfaz de la biblioteca estándar. Usando las clases CWndContainer y CWnd como parámetros de la plantilla, determinamos la clase intermedia StdLayoutBase.

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

El método setContainer determina con la ayuda de la conversión dinámica de tipos si el elemento CWnd es heredero de CDialog o CBox, y si es así, entonces se tratará de un contenedor.

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

El método create inicializa un elemento y retorna su nombre. Preste atención: el trabajo se realiza solo con el gráfico actual (ChartID()), y solo en la ventana principal (la subventana no se tiene en cuenta en el marco del presente proyecto, pero si el lector lo desea, podrá adaptar el código a sus necesidades).

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

El método add añade el elemento hijo al elemento padre, realizando preliminarmente el mayor "upcasting" posible, puesto que el método Add en la biblioteca estándar no es un método virtual (en principio, podríamos introducir la corrección correspondiente en la biblioteca estándar, pero hablaremos más adelante de su modificación).

Usando como base la clase StdLayoutBase, creamos la clase de trabajo _layout, que figurará en el código con la descripción de la disposición en MQL. El nombre comienza con guión bajo, para resaltar la designación no estándar de los objetos de esta clase. Vamos a echar por ahora un vistazo a la versión simplificada de la clase. Más tarde, le añadiremos funcionalidad adicional. El trabajo al completo es iniciado en la práctica por los constructores, dentro de los cuales se llama precisamente este u otro método init desde LayoutBase.

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

Podemos abarcar la imagen general con la ayuda del siguiente diagrama. En este, hay alguna cosa con la que debemos familiarizarnos, pero la mayoría de las clases ya las conocemos.

Diagrama de clases de la disposición de GUI

Diagrama de clases de la disposición de GUI

Ahora, ya hemos podido comprobar en la práctica cómo la descripción de un objeto, por ejemplo, _layout<CButton> button(m_button, 100, 20) inicializa y registra el objeto m_button en la ventana de diálogo, con la condición de que aquel esté descrito en el bloque externo aproximadamente de esta forma: _layout<CAppDialog> dialog(this, name, x1, y1, x2, y2). No obstante, los elementos tiene muchas otras propiedades, aparte de las dimensiones. Algunas propiedades, tales como la alineación lateral, son no menos importantes que las coordenadas. En realidad, si un elemento tiene alineación (alignment, en la terminología de la biblioteca estándar) horizontal, será estirado a todo el ancho de la zona del contenedor padre, descontando los márgenes establecidos a izquierda y derecha. De esta forma, la alineación tiene prioridad ante las coordenadas. Más aún, en los contenedores de la clase CBox, es importante la orientación (dirección) en la que se realiza la colocación de los elementos hijos: horizontal (por defecto) o vertical. También sería correcto dar soporte a otras propiedades que influyen en el aspecto exterior (como el tamaño de la fuente, el color) y el modo de funcionamiento (por ejemplo, solo lectura, teclas especiales, etcétera).

En los casos en los que un objeto de GUI fuera descrito en una clase de ventana y transmitido a la disposición, podríamos usar los métodos "propios" de establecimiento de propiedades (por ejemplo, edit.Text("text")). El sistema de disposición da soporte a este antiguo método, pero no es el único óptimo. En muchos casos, resulta cómodo asignar la creación de objetos al sistema de disposición, y entonces aquellos no estarán disponibles directamente desde la ventana. De esta forma, resulta necesario ampliar de alguna forma las posibilidades de la clase _layout en cuanto al ajuste de elementos.

Dado que hay muchas propiedades, sería mejor no cargar a la misma clase con todo el trabajo referente a ellas, sino dividir la responsabilidad entre ella y una clase especial. En este caso, además, _layout seguirá siendo el "punto de entrada" para registrar los elementos, pero delegará todos los detalles de la configuración en la nueva clase. Esto es importante además para hacer la tecnología de disposición lo más independiente posible de una biblioteca concreta de elementos de control.

Clases para ajustar las propiedades de los elementos

A nivel abstracto, un conjunto de propiedades se divide según el tipo de valor. Vamos a dar soporte a los principales tipos incorporados del lenguaje MQL, y también a otros de los que hablaremos más tarde. Desde un punto de vista sintáctico, resultaría cómodo asignar las propiedades a través de la cadena de llamadas del famoso patrón "constructor" (builder):

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

Sin embargo, esta sintaxis presupone un conjunto de métodos muy largo en una sola clase; además, esta clase debe ser la clase de la disposición, dado que el operador de denominación (el punto) no se puede redefinir. Podemos reservar en la clase _layout un método para retornar un ejemplar del objeto auxiliar para las propiedades, más o menos así:

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

Pero entonces, sería adecuado determinar muchas clases-intermediarios, cada una para su tipo de elemento, para comprobar en la etapa de compilación si las propiedades asignadas son correctas. Esto cpmplicaría el proyecto, pero, para una primera implementación de prueba, querríamos hacerlo todo bastante simple (lo más posible). En general, hemos dejado este enfoque para el futuro.

Asimismo, debemos destacar que los nombres de los métodos en la plantilla "de construcción" son innecesarios en cierto sentido, dado que los valores del tipo LAYOUT_STYLE_VERTICAL o clrGray "hablan" por sí mismos, y además, estos tipos no necesitan mayor nivel de detalle: así, para el "control " CEdit, el valor del tipo bool normalmente indica la bandera "solo lectura", mientras que para CButton indica el signo de "tecla especial". Como resultado, parece que la solución podría consistir simplemente en asignar los valores con la ayuda de algún operador sobrecargado. Pero el operador de asignación, por extraño que parezca, no nos sirve, porque no permite enlazar la cadena de llamadas.

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

Los operadores de asignación de una sola línea se ejecutan de derecha a izquierda, es decir, no parten del objeto en el que se ha introducido la asignación sobrecargada. Funcionaría así:

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

Pero esto no tiene un aspecto demasiado bonito.

La variante:

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

también es demasiado larga. Por eso, hemos tomado la decisión de sobrecargar el operador <= y usarlo más o menos así:

  column <= LAYOUT_STYLE_VERTICAL <= clrGray <= 5.0;

Para ello, en la clase LayoutBase tenemos un stub:

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

Su objetivo es doble: declarar la intención de usar la sobrecarga del operador y recordar redefinir el método en una clase derivada. En teoría, allí se debe aplicar un objeto de clase-intermediaria con la siguiente interfaz (no se muestra al completo).

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

Como podemos ver, en la clase-intermediaria se guarda el enlace al elemento (object) que debe ser ajustado. La vinculación se realiza en el constructor, o con la ayuda del método assign. Si suponemos que tenemos escrito un cierto intermediario concreto de la clase MyControlProperties:

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

en la clase _layout podremos usar su objeto según este esquema (hemos añadido líneas y un método comentados):

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

Gracias a que el operador <= en _layout es un operador de plantilla, este genera automáticamente la llamada para el tipo de parámetro correcto de la interfaz ControlProperties (claro está que no hablamos de los métodos abstractos de la interfaz, sino de sus implementaciones en la clase derivada MyControlProperties; pronto escribiremos una así para una biblioteca de ventana concreta).

En algunos casos, el mismo tipo de datos se usa para establecer varias propiedades distintas. Por ejemplo, el mismo bool se usa en CWnd al establecer las banderas de visibilidad y actividad de los elementos, aparte de en los modos "solo lectura" (para CEdit) y teclas especiales (para CButton), mencionados más arriba. Para que podamos indicar explícitamente el nombre de una propiedad, en la interfaz ControlProperties se ha pensado el operador[] con el parámetro del tipo de línea. Este establece el campo context, sobre cuya base, la clase derivada podrá cambiar la característica necesaria.

Para cada combinación del tipo de datos de entrada y la clase del elemento, una de las propiedades (la utilizada con mayor frecuencia) se considerará propiedad por defecto (sus ejemplos para CEdit y CButton se muestran más arriba). Las demás propiedades requerirán que se indique el contexto.

Por ejemplo, para el botón CButton, esto tendrá el aspecto que sigue:

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

En la primera línea, el contexto no ha sido indicado y por eso se presupone la propiedad "locking" (botón de dos posiciones). En la segunda, el botón se crea inicialmente como invisible (lo cual se requiere con muy poca frecuencia).

Vamos a analizar los principales matices en la implementación del intermediario StdControlProperties para la biblioteca de elementos estándar. El lector podrá familiarizarse con el código completo en los archivos adjuntos. Al principio, podemos ver cómo se redefine el operador <= para el tipo bool.

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

Para las líneas, se aplica la regla siguiente: cualquier texto entrará en el encabezado del "control" si solo no se ha establecido el contexto "font", que indica el nombre de la fuente:

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

En la clase StdControlProperties, se han introducido de forma adicional las redefiniciones <= para aquellos tipos propios solo para la biblioteca estándar. Enconcreto, sabe adoptar la enumeración ENUM_WND_ALIGN_FLAGS, que describe la variante de alineación. Preste atención a que, en esta enumeración, además de los cuatro lados (izquierda, derecha, arriba y abajo), se describen solo las combinaciones más utilizadas (pero no todas en general), tales como la alineación a lo ancho (WND_ALIGN_WIDTH = WND_ALIGN_LEFT|WND_ALIGN_RIGHT) o en toda la zona de cliente (WND_ALIGN_CLIENT = WND_ALIGN_WIDTH|WND_ALIGN_HEIGHT). No obstante, si necesitamos alinear un elemento a lo ancho y en el borde superior, esta combinación de banderas ya no será parte de la enumeración. Por eso, necesitaremos indicar explícitamente la conversión del tipo a ella ((ENUM_WND_ALIGN_FLAGS)(WND_ALIGN_WIDTH|WND_ALIGN_TOP)). En caso contrario, la operación de "O" a nivel de bits dará el tipo int, y se llamará la sobrecarga incorrecta para establecer las propiedades de tipo entero. Podemos considerar una alternativa la indicación del conexto "align".

Lo más laborioso, como es de esperar, resultará redefinir el tipo int. En concreto, aquí se pueden establecer la anchura, la altura, los márgenes, el tamaño de la fuente y otras propiedades. Para aligerar esta situación, se ha implementado la posibilidad de indicar el tamaño directamente en el constructor del objeto de disposición, mientras que los márgenes se pueden establecer alternativamente con la ayuda de cifras del tipo double o con la combinación especial PackedRect. Claro está que también se ha añadido para ella la sobrecarga del operador, tan cómoda de usar cuando necesitamos márgenes asimétricos:

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

porque los márgenes iguales en todos sus lados se pueden establecer con mayor facilidad con un valor double:

  button <= 5.0;

No obstante, también existe una alternativa para la selección del usuario, el contexto "margin", que hace innecesario double; la entrada equivalente será:

  button["margin"] <= 5;

Con respecto a los márgenes y sangrías, debemos prestar atención a un detalle. En la biblioteca estándar existe el término alineación (alignment), que incluye los márgenes (margins) añadidos automáticamente alrededor del "control". Además de ello, en las clases de la familia CBox se ha implementado un mecanismo de sangrías (padding) propio, en el que estas suponen una cierta separación entre su borde exterior y los "controles" hijos (contenidos). De esta forma, los márgenes (desde el punto de vista de los "controles") y las sangrías (desde el punto de vista de los contenedores), en esencia, son lo mismo. Y, dado que los dos algoritmos de posicionamiento, por desgracia, no se tienen en cuenta el uno al otro, el uso simultáneo de márgenes y sangrías puede crear problemas (el más obvio sería un desplazamiento de los elementos que no se correspondería con lo esperado). La recomendación general es dejar las sangrías igual a cero y manipular los márgenes. Sin embargo, según las circunstancias, podemos probar a activar también las sangrías, sobre todo si hablamos de un contenedor concreto, y no de los ajustes generales.

Este artículo es una investigación del tipo "prueba de concepto" (proof of concept, POC), y no ofrece una solución preparada. Su tarea consiste en poner a prueba la tecnología ofrecida -al momento en que se escribe el material- en las clases de la biblioteca estándar y los contenedores, con unas correcciones mínimas de todos estos componentes. De forma ideal, los contenedores (no necesariamente CBox) deben escribirse como una parte inseparable de la biblioteca de elementos de GUI, y actuar teniendo en cuenta todas las posibles combinaciones de modos.

Más abajo, mostramos un recuadro con las propiedades y elementos soportados. La clase CWnd indica la aplicabilidad de las propiedades a todos los elementos; la clase CWndObj, para los "controles" simples (dos de ellos, CEdit y CButton, también se indican en el recuadro). La clase CWndClient generaliza los "controles" compuestos (CCheckGroup, CRadioGroup, CListView), y también es la clase padre para los contenedores CBox/CGrid.

Recuadro de propiedades soportadas según el tipo de datos y la clase de elementos

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


El código fuente completo de la clase StdControlProperties, que posibilita la transmisión de las propiedades de los elementos de disposición a las llamadas de los métodos de la biblioteca de componentes estándar, se adjunta al artículo.

Vamos a probar las clases de disposición en la práctica. Al fin, podremos comenzar a analizar los ejemplos, partiendo de lo simple hacia lo complejo. Según la tradición nacida de los dos artículos anteriores sobre la composición de GUI con ayuda de contenedores, vamos a adaptar a la nueva tecnología el juego del 15 o rompecabezas deslizante (SlidingPuzzle4) y la demo estándar para trabajar con los "controles" (ControlsDialog4). Los índices se corresponden con las etapas de actualización de estos proyectos. En el otro artículo, los mismos programas se presentan con los índices 3; si el lector lo desea, podrá comparar los códigos fuente. Los ejemplos se encuentran en la carpeta MQL5/Experts/Examples/Layouts/.

Ejemplo 1. El rompecabezas deslizante SlidingPuzzle

El único cambio sustancial en la interfaz pública del formulario principal CSlidingPuzzleDialog es la aparición del nuevo método CreateLayout. Es mejor llamarlo desde el manejador OnInit, y no desde Create. La lista de parámetros en ambos métodos es la misma. Esta sustitución era necesaria porque la propia ventana de diálogo es un objeto de disposición (del nivel más externo) y su método Create será llamado automáticamente por el nuevo marco de trabajo (esto lo hace el método StdLayoutBase::create, que hemos analizado anteriormente). Mientras tanto, toda la información para el marco de trabajo sobre el formulario y su contenido se establece precisamente en el método CreateLayout, con la ayuda de un lenguaje de marcado basado en MQL. Aquí tenemos el propio método:

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

Aquí, se formulan secuencialmente dos contenedores incorporados; cada uno de ellos es controlado por su propio objeto de disposición:

  • dialog para el ejemplar CSlidingPuzzleDialog (variable this);
  • clientArea para el elemento CGridTkEx m_main;

A continuación, en la zona de cliente se inicializa el conjunto de botones CButton m_buttons[16] vinculado al objeto de disposición único block, el botón para comenzar a jugar (CButton m_button_new en el objeto start) y la etiqueta de información (CEdit m_label, objeto label). Todas las variables globales (dialog, clientArea, block, start, label) posibilitan la llamada automática de Create para los elementos de la interfaz en el orden de ejecución del código, les asignan los parámetros adicionales establecidos (hablaremos con más detalle sobre los parámetros un poco más abajo), y en el momento de la eliminación, es decir de la salida del ámbito del siguiente bloque de llaves, se registran los elementos de interfaz relacionados con ellos en el contenedor situado más arriba. De esta forma, la zona de cliente m_main se incluirá en la ventana this, mientras que todos los "controles" se incluirán en la zona de cliente. Bien es cierto que, en este caso, el orden de inclusión será inverso, es decir, los bloques se cerrarán comenzando por el más anidado. Pero este no es el asunto. En el modo acostumbrado de creación de ventanas de diálogo, sucede más o menos lo mismo: los grupos más grandes de la interfaz crean grupos más pequeños, estos, a su vez, crean otros más pequeños, hasta llegar al nivel de los "controles", y comienzan a añadir los elementos inicializados en orden inverso (ascendente): primero, los "controles" se añaden a los bloques medianos, y después los medianos se añaden a los grandes.

Para la ventana de diálogo y la zona de cliente, todos los parámetros se transmiten a través de los parámetros de los constructores (es semejante al método Create estándar). No necesitamos transmitir los tamaños a los "controles", dado que estos son ubicados correctamente de forma automática por la clase GridTkEx, mientras que los otros parámetros se transmiten con la ayuda del operador <=.

El bloque de 16 botones se inicializa sin ciclo visible (ahora está oculto en el objeto de disposición). El color de fondo de todos los botones se establece con la línea block["background"] <= clrCyan. A continuación, se transmiten al mismo objeto de disposición los objetos auxiliares (SimpleSequenceGenerator), que aún descnocemos.

Al formar la interfaz de usuario, con frecuencia surge la necesidad de generar varios elementos de un mismo tipo y rellenar estos por paquetes con ciertos datos conocidos. Para este objetivo, resulta cómodo usar el así llamado generador.

Un generador es una clase con un método que se puede llamar en un ciclo para obtener el siguiente elemento de una cierta lista.

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

Normalmente, el generador debe conocer el número de elementos necesarios, y también saber guardar el cursor (el índice del elemento actual). En concreto, si debemos crear una secuencia de valores de cierto tipo incorporado (por ejemplo, entero o string), nos convendrá esta implementación simple de SimpleSequenceGenerator.

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

Los generadores han sido añadidos para realizar con mayor comodidad las operaciones por paquetes (archivo Generators.mqh), mientras que en la clase de disposición, tenemos el operador predeterminado <= para los generadores. Gracias a ello, podemos rellenar en una sola línea 16 botones con identificadores y encabezados.

En las siguientes líneas del método CreateLayout, se crea el botón m_button_new.

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

La línea "New" es tanto el identificador, como el encabezado. Si necesitáramos asignar otro encabezado, podríamos hacerlo así: start <= "Caption". En principio, tampoco es obligatorio establecer el identificador (si no lo necesitamos), el sistema lo generará por sí mismo.

En la segunda línea, establecemos un contexto que contiene dos pistas al mismo tiempo: background y font. La primera es necesaria para interpretar correctamente el color, clrYellow. Dado que el botón es heredero de CWndObj, para él, el color "anónimo" indicará el color del texto. La segunda pista garantiza que la línea "Arial Black" cambiará la fuente utilizada (sin contexto, la línea cambiaría el encabezado). Si alguien lo desea, podrá escribir con detalle:

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

Claro está que sus métodos siguen disponibles para el botón, es decir, se puede escribir como antes:

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

No obstante, para ello deberemos tener un objeto de botón, y eso no será siempre así: a continuación, veremos un esquema en el que la disposición comenzará a encargarse de todo, incluyendo la construcción y el guardado de elementos.

Para ajustar las etiquetas, se usan estas líneas:

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

Precisamente aquí, se crea el objeto con el identificador automático (si abrimos la ventana con la lista de objetos en el gráfico, veremos el número único del ejemplar). En la segunda línea, se establece el texto de la etiqueta, el signo de "solo lectura" y la alineación del texto en la parte media.

A continuación, le siguen las líneas de ajuste del objeto m_main de la clase CGridTKEx:

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

CGridTKEx supone un CGridTk ligeramente mejorado (ya lo conocemos de los artículos anteriores). En CGridTkEx, se ha implementado un método que establece las limititaciones para los "controles" hijos con la ayuda del nuevo método SetGridConstraints. En GridTk, es posible hacer esto solo de forma simultánea, añadiendo un elemento dentro del método Grid. Esto resulta negativo en sí, ya que ubica en un solo método dos operaciones en esencia distintas: el establecimiento de las relaciones entre objetos y el ajuste de propiedades. Además, resulta que debemos añadir los elementos a una cuadrícula, pero no con la ayuda de Add, sino solo con este método (dado que es el único modo de establecer las limitaciones sin las cuales GridTk no peuede funcionar). Esto contradice el enfoque general de la biblioteca, donde, para estos cometidos, siempre se usa Add. Y con esto, a su vez, se relaciona el funcionamiento del sistema de marcado automático. En la clase CGridTkEx, hemos separado las 2 operaciones, cada una de ellas tiene ahora su propio método.

Recordemos que, para los grandes contenedores (que incluyen toda la ventana) de las clases CBox/CGridTk, es importante llamar al método Pack: precisamente él realiza la disposición, llamando en caso necesario a Pack en los contenedores incorporados.

Si comparamos los códigos fuente de SlidingPuzzle3.mqh y SlidingPuzzle4.mqh, veremos con facilidad que el código fuente es ahora notablemente más compacto. En la clase ya no se encuentran los métodos Create, CreateMain, CreateButton, CreateButtonNew, CreateLabel. En lugar de ellos, ahora solo se encuentra CreateLayout.

Una vez iniciado el programa, podemos comprobar que los elementos se crean y funcionan como habíamos planeado.

Sí que es cierto que seguimos teniendo en la clase una lista con la declaración de todos los "controles" y contenedores. Y, a medida que se vayan complicando los programas y vaya aumentando el número de componentes, no resultará muy cómodo duplicar su descripción en la clase de la ventana y en la disposición. ¿Acoso no es posible hacerlo todo con la ayuda de la disposición? No resulta difícil suponer que sí. Pero hablaremos de ello en la segunda parte de la publicación.

Conclusión

En el presente artículo, nos hemos familiarizado con las bases teóricas y el cometido de los lenguajes de marcado de las interfaces gráficas, hemos desarrollado el concepto de implementación del lenguaje de marcado con MQL y hemos analizado las principales clases que plasman esa idea en la vida real. No obstante, aún nos quedan muchos ejemplos complejos y constructivos por ver.

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

Archivos adjuntos |
MQL5GUI1.zip (86.86 KB)
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.
Optimización móvil continua (Parte 5): Panorámica del proyecto del optimizador automático, creación de la interfaz gráfica Optimización móvil continua (Parte 5): Panorámica del proyecto del optimizador automático, creación de la interfaz gráfica
Continuamos con la descripción de la optimización móvil en el terminal MetaTrader 5. Tras analizar en los artículos anteriores los métodos de formación del informe de optimización y su método de filtrado, hemos procedido a describir la estructura interna de la aplicación encargada del propio proceso de optimización. El optimizador automático, ejecutado como una aplicación en C#, tiene su propia interfaz gráfica. Este artículo está dedicado precisamente a esta interfaz gráfica.
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".
¡Los proyectos ayudan a crear robots rentables! O eso parece ¡Los proyectos ayudan a crear robots rentables! O eso parece
Un gran programa comienza con un pequeño archivo, que a su vez va creciendo y llenándose con multitud de funciones y objetos. La mayoría de los programadores de robots afronta este problema con la ayuda de archivos de inclusión. Pero resulta mejor comenzar a escribir cualquier programa para trading directamente en un proyecto: es más rentable en todos los sentidos.