English Русский 中文 Deutsch 日本語 Português
Desarrollando las interfaces gráficas a base de .Net Framework e C# (Parte 2): Elementos gráficos adicionales

Desarrollando las interfaces gráficas a base de .Net Framework e C# (Parte 2): Elementos gráficos adicionales

MetaTrader 5Ejemplos | 23 julio 2019, 13:18
2 100 0
Vasiliy Sokolov
Vasiliy Sokolov

Índice


Introducción

Desde el octubre de 2018, MetaTrader 5 comenzó a ofrecer el soporte de la integración con las bibliotecas de Net Framwork. En realidad, este conjunto de bibliotecas es algo mucho más que cualquier framwork o un sistema especializado para ejecutar ciertas tareas, por ejemplo, el dibujado de ventanas gráficas o implementación de la interacción de red. En Net Framework hay absolutamente todo. Puede utilizarlo para diseñar los sitios web (Net Core, MVC), desarrollar las aplicaciones de sistema con una interfaz profesional y unificada (Windows Forms), crear unos sistemas distribuidos complejos con intercambio de información entre los nodos (Windows Comunication Fundation), trabajar con bases de datos (Entity Framework). Finalmente, Net Framework es una enorme comunidad de programadores y empresas con millares de proyectos con código abierto de diverso contenido. Todo eso, con una debida interacción, ahora puede estar disponible en MQL.

En este artículo, seguiremos desarrollando la funcionalidad de GuiController, creado en la primera parte. Esta funcionalidad está orientada a la interacción con la funcionalidad gráfica de Net Framework a base de la tecnología Windows Forms. Hay muchas publicaciones sobre las posibilidades de MQL. Actualmente, existe una gran variedad de diferentes bibliotecas que, en cierta medida, hacen algo parecido, pero usando los recursos de MQL. Por eso, no me gustaría que este material se interpretase por los lectores como «una biblioteca más para trabajar con los formularios». En realidad, este material es sólo una parte de una serie grande de los artículos que describe la interacción con Net Framework y que descubre paso a paso el Universo ilimitado de la plataforma de software. Windows Forms es sólo uno de los bloques, aunque sea muy conveniente y universal, como cualquier otra parte de la tecnología Net. El subsistema gráfico de Windows Forms es un perfecto punto de partida para explorar esta estructura. Después de estudiar, se puede aplicar Windows Forms en otras áreas de interacción con Net Framework. También permite crear los paneles comerciales, ventanas de configuración de los EAs, indicadores gráficos avanzados, sistemas de gestión de los robots bastante efectivos y, lo más importante, simples de implementar y todo lo que está relacionado con la interacción entre el usuario y la plataforma comercial.

No obstante, para implementar todas estas posibilidades atractivas, hace falta complementar significativamente el módulo de la interacción entre el programa MQL y la biblioteca C#. Como puede recordar, en la primera parte, nuestro módulo GuiController era capaz interactuar sólo con algunos elementos gráficos de WinForms, como (Button), etiquetas de texto (Label), campos de entrada para introducir el texto (TextBox) y scroll vertical. A pesar de este soporte escaso de los elementos, conseguimos crear un panel gráfico completo y bastante funcional:

Fig. 1. Panel comercial creada en la primera parte del artículo

A pesar de un resultado bastante impresionante, no vamos a parar en lo conseguido y seguiremos mejorando nuestro controlador. En esta parte, lo proveeremos con los elementos gráficos adicionales, a través de los cuales ya podremos crear la mayoría de los tipos de formularios.


Organización del testeo de nuevos elementos

Para introducir el soporte de nuevos elementos, es necesario organizar una especie de la «instalación de ensayos». Eso es necesario para depurar el trabajo con nuevos elementos y eliminar los errores emergentes y potenciales que aparecen durante la introducción de una funcionalidad nueva. Nuestra «instalación de ensayos» va a componerse de nuestro controlador, formularios con conjuntos necesarios de los elementos gráficos y un Asesor Experto que va a procesar estos elementos. Todos los formularios van a ubicarse dentro del mismo ensamblado DemoForm.exe. Dentro del EA, crearemos un parámetro especial personalizado que va a indicarle qué formulario gráfico debe cargarse desde DemoForm.exe:

 

Fig. 2 Selección del formulario personalizado con elementos necesarios

El EA en sí será bastante simple. Esencialmente, tendrá dos partes: la función del cargamento (función de inicialización OnInit estándar) y el procesador de eventos gráficos (ciclo de la iteración en la función OnTimer). Recordaremos que el trabajo con GuiController se realiza a través de la llamada a los métodos estándar. Los métodos principales son cuatro:

  1. Show Form - ejecuta el formulario desde un ensamblado especificado;
  2. HideForm - oculta el formulario;
  3. GetEvent - obtiene el evento desde el formulario;
  4. SendEvent - envía el evento al formulario.

Cargamos la ventana necesaria en la función OnInit dependiendo del elemento seleccionado. El prototipo de la función se muestra a continuación:

int OnInit()
{
   switch(ElementType)
   {
      case WINFORM_TAB:
         GuiController::ShowForm("DemoForm.exe", "tab_form");
         break;
      case WINFORM_BUTTON:
         GuiController::ShowForm("DemoForm.exe", "button_form");
         break;
      ...
   }
   ...
}

Los eventos que llegan del formulario se procesan en la función OnTimer:

//+------------------------------------------------------------------+
//| Timer function                                                   |
//+------------------------------------------------------------------+
void OnTimer()
{   
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      ...
      if(id == TabIndexChange)
         printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);
      else if(id == ComboBoxChange)
         printf("ComboBox '" + el_name + "' was changed on " + sparam);
      ...
   }
}

Así, para cada uno de los elementos gráficos, creamos un breve ejemplo de trabajo para interactuar con él. También describiremos detalladamente los eventos que soporta.


MessageBox (Ventanas de mensajes)

Partiendo de la segunda versión, el controlador comenzó a soportar las ventanas de los mensajes MessageBox. Es un elemento estándar para informar al usuario. También permite ofrecer al usuario varias opciones de acción y obtener una respuesta en forma de una opción escogida por él.

Para iniciar la visualización de las ventanas de los mensajes, seleccione la opción 'Buttons and MessageBox' en el parámetro "Windows Form Element Type" al iniciar el EA. Después de iniciar el EA, aparece el formulario que propone seleccionar una de las siguientes opciones:

Fig. 3. Formulario demo que llama a la ventana de mensajes 

Este formulario, como todos los siguientes, es de demostración, por tanto, está dotado con la lógica comercial. Sin embargo, tras la pulsación de cualquier botón, el EA enviará un aviso que solicita la confirmación de las acciones seleccionadas. Por ejemplo, al pulsar el botón SELL, se mostrará la siguiente ventana de los mensajes:


Fig. 4. El EA solicita la confirmación para abrir una nueva posición corta.

Después de que el usuario haga clic en uno de los botones, el evento de su pulsación se guarda y se registra en el búfer de eventos GuiController. El EA accede a este búfer con una periodicidad especificada, y en cuanto descubra que el búfer ha recibido un nuevo evento, comienza su procesamiento. De esta manera, el EA necesita recibir el evento «Pulsar del botón» y reaccionar enviando el evento contrario 'MessageBox'.

for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      //-- Obtenemos un nuevo evento
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      //-- Sabemos su tipo - pulsar el botón
      if(id == ClickOnElement)
      {
         //-- Visualizar el nombre del botón presionado en la consola del terminal
         printf("You press '" + sparam + "' button");
         string msg;
         //-- Dependiendo del tipo del botón pulsado, ha sido formado el mensaje MessageBox
         if(el_name != "btnCancelAll")
            msg = "Are you sure you want to open a new " + sparam + " position?";
         else
            msg = "Are you sure you want to close all positions?";
         //-- Hemos enviado el mensaje contrario con el comando para mostrar MessageBox
         GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);
      }
      ...
   }

Analizaremos la signatura del envío del evento:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OKCancel, msg);

Eso significa que el EA solicita visualizar la ventana de diálogo (MessageBox) con el texto principal en la variable msg, mostrando dos botones OK y Cancel (OKCancel). En la primera parte del artículo, hemos dicho que el primer parámetro del método SendEvent contiene el nombre del elemento gráfico del destinatario del evento que se envía. No obstante, en caso de MessageBox, este campo funciona de forma diferente. Resulta que las ventanas de diálogo no están vinculadas a una determinada ventana gráfica o elemento (aunque Windows Frorms permite hacer esta vinculación). Por eso, GuiController crea una nueva ventana de mensajes por su cuenta, y no necesita la dirección del destino del mensaje. Sin embargo, generalmente, cuando se muestra un mensaje, es necesario bloquear la ventana a la que corresponde. Es verdad, sería extraño si al mostrar un mensaje, fuera posible pulsar el botón BUY o SELL de nuevo, ignorando MessageBox que aparece. Por eso, en GuiController, el nombre del primer parámetro para este evento significa el nombre del elemento que tiene que ser bloqueado hasta el momento de la pulsación uno de los botones MessageBox por el usuario. La función del bloqueo de un elemento gráfico aleatorio es opcional. Se define a través de la variable entera lparam: 0 - no hay bloqueo de la ventana, 1 - con bloqueo. No obstante, es más conveniente operar con constantes, en vez de los ceros y unos. Para este propósito, en GuiController han sido definidas dos constantes a través de la enumeración BlockingControl:

  • LockControl; 
  • NotLockControl

La primera bloquea la ventana hasta que el usuario pulse el botón, mientras que la segunda no hace nada, permitiendo a la ventana gráfica seguir estando disponible para el usuario.

Además del texto, las ventanas de diálogo también pueden contener diferentes combinaciones de botones. Pulsando estos botones, el usuario acepta una u otra opción de la selección. Los conjuntos de los botones se establecen usando la enumeración de sistema System.Windows.Forms.MessageBoxButtons. Los elementos de esta enumeración no están disponibles para los usuarios MQL, porque están definidos en el ensamblado externo. Para facilitar el trabajo de los programadores de MQL con GuiController, fue introducida una nueva enumeración, es decir, el clon System.Windows.Forms.MessageBoxButtons con los mismos parámetros. La definición de esta enumeración se da en IController.cs:

//
// Summary:
//     Specifies constants defining which buttons to display on a System.Windows.Forms.MessageBox.
public enum MessageBoxButtons
{
    //
    // Summary:
    //     The message box contains an OK button.
    OK = 0,
    //
    // Summary:
    //     The message box contains OK and Cancel buttons.
    OKCancel = 1,
    //
    // Summary:
    //     The message box contains Abort, Retry, and Ignore buttons.
    AbortRetryIgnore = 2,
    //
    // Summary:
    //     The message box contains Yes, No, and Cancel buttons.
    YesNoCancel = 3,
    //
    // Summary:
    //     The message box contains Yes and No buttons.
    YesNo = 4,
    //
    // Summary:
    //     The message box contains Retry and Cancel buttons.
    RetryCancel = 5
}

Las constantes de esta enumeración están disponibles en MQL Editor directamente, por ejemlo, a través de IntelliSens, lo que hace que la configuración de MessgaBox sea bastante cómoda. Por ejemplo, si en SendEvent sustituimos la constante OKCancel por YesNoCancel, la ventana de diálogo ya tendrá otro conjunto de botones:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, YesNoCancel, msg);

Fig. 5. Combinación estándar de tres botones de selección - Yes/No/Cancel.

A parte de las combinaciones de botones GuiController, soporta la configuración de los iconos de mensajes y del texto del encabezado de la ventana. Como el método SendEvent tiene un numero fijo de parámetros, resulta bastante problemático pasar todas las configuraciones a través de él, por eso, hemos encontrado una solución alternativa. La línea del texto puede ser dividida en secciones a través del símbolo " |" En este caso, cada sección será responsable de su parámetro adicional. Puede haber de una (sin separadores) a tres secciones (dos separadores). Vamos a ver varios ejemplos. Supongamos que es necesario mostrar un mensaje simple, sin icono y instrucción adicional. Entonces, el formato del mensaje será el siguiente:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "This is a simple message");


Fig. 6. Mensaje simple sin iconos ni texto adicional en el encabezado de la ventana.

Se puede añadir un icono usando una constante especial en la sección adicional. Supongamos que queremos mostrar un mensaje con icono de aviso "Warning", entonces, el formato en el texto del mensaje será el siguiente:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "Warning|Your action can be dangerous");

Fig. 7. Mensaje de advertencia

Se puede definir el icono no sólo a través de una palabra clave, sino también con un icono de seudónimo. Por ejemplo, si en vez de Warning escribimos "?", el efecto será el mismo. Además del icono Warning, se puede definir el icono de información, pregunta y error. Mostramos una tabla con palabras clave y seudónimos para estos iconos:

Icono Palabra clave Seudónimo

Warning !

Error !!!

Info i

Question  ?


Además de los iconos, se puede definir el nombre de la propia ventana de diálogo. Para eso, es necesario separar el texto usando la sección "|", e insertar el nombre de la ventana. Aquí tenemos un ejemplo de una definición completa de la ventana con un mensaje de error:

GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, "!!!|The operation was cancelled|Critical Error");

Fig. 8. Visualización de la ventana de mensajes con icono del error y el nombre de la ventana

El controlador analiza la línea de forma intelectual. Si especificamos la cadena "!!!|The operation was cancelled", se mostrará el icono del error crítico con mensaje correspondiente. Si se indican dos secciones en la cadena "The operation was cancelled|Critical Error", el icono no se mostrará, pero el nombre de la ventana será cambiado por "Critical Error".


TabControl (Pestañas)

Las pestañas o Tabs representan una herramienta cómoda para organizar los elementos en grupos:

Fig. 9. Panel con dos pestañas

El control de la pestaña soporta el único evento TabIndexChange. Sirve para informar que el usuario pasa a otra pestaña. En el EA de prueba, hay un código que restrea el cambio de la tabulación en el formulario. Mostramos un fragmento:

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == TabIndexChange)
     printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);
}

El evento TabIndexChange transmite dos parámetros: lparam y sparam. El primero es el índice de la pestaña seleccionado por el usuario. El segundo contiene el nombre de la pestaña seleccionada. Por ejemplo, si el usuario selecciona la primera pestaña, el EA mostrará el mensaje:

Selecet new tab. Index: 0 Name: tabPage1

Las pestañas representan un elemento gráfico extremadamente útil por sí sólo. En realidad, no siempre es oportuno rastrear el cambio de la tabulación. Es que WindowsForm exige que todos los elementos del mismo formulario tengan nombres únicos. Por tanto, dos elementos del mismo tipo, pero ubicados en pestañas diferentes, serán únicos, lo que significa que tendrán que poseer nombres diferentes, desde el punto de vista de WindowsForm. Por otro lado, por regla general, hay que rastrear la pulsación de los controles determinados, no siempre es importante saber en qué pestaña se encuentra este elemento. No obstante, a veces es necesario rastrear estos eventos, por eso, GuiController proporciona una interfaz de interacción necesaria y suficiente para este elemento.

CheckBox (Casilla de verificación)

ChreckBox o casilla de verificación es uno de los elementos clave de cualquier interfaz gráfica. A pesar de su sencillez, se utiliza en en una amplia variedad de interfaces , empezando con las versiones antiguas de Windows y terminando con las aplicaciones Web y móviles. Permite indicar una opción de forma comprensible intuitivamente. Además, se puede usarla para mostrar las opciones de la selección que no están disponibles por alguna razón, dando al usuario la posibilidad de escoger opciones que no contradicen una a otra.

 

Fig. 10 Opciones de selección a través de una combinación de chekboxes

El chekbox tiene tres estados: marcado (Checked), desmarcado (Unchecked) y marcado parcialmente (Indeterminate). En Windows Forms hay una estructura System.Windows.Forms.CheckState que describe estos estados:

namespace System.Windows.Forms
{
    //
    // Summary:
    //     Specifies the state of a control, such as a check box, that can be checked, unchecked,
    //     or set to an indeterminate state.
    public enum CheckState
    {
        //
        // Summary:
        //     The control is unchecked.
        Unchecked = 0,
        //
        // Summary:
        //     The control is checked.
        Checked = 1,
        //
        // Summary:
        //     The control is indeterminate. An indeterminate control generally has a shaded
        //     appearance.
        Indeterminate = 2
    }
}

Cada vez cuando el usuario pulse este chekbox, GuiController transmite su estado al EA MQL usando el evento CheckBoxChange, a través de la variable lparam. Sus valores corresponden a una de las opciones de esta enumeración: 0 — Unchecked, 1 — Checked, 2 — Indeterminate.

En el ejemplo demo, el EA rastrea la selección de las casillas de verificación  'Enable Trading On EURUSD' 'Enable Trading On GBPUSD'. En cuanto uno de los puntos se haga disponibles, él hace que sus subpuntos también estén disponibles: 'Allow take profit' y 'Allow stop loss'. Por otro lado, si el usuario desmarca uno de los puntos, sus subpuntos se desactivan. Eso se consigue gracias a dos eventos: ElementEnable y CheckBoxChange. En el código de abajo, se muestra el algoritmo del trabajo del EA con los chekboxes.

void OnTimer()
{   
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
      int id;
      string el_name;
      long lparam;
      double dparam;
      string sparam;
      GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
      if(id == CheckBoxChange)
         ChangeEnableStopAndProfit(el_name, id, lparam, dparam, sparam);
   }
}

//+------------------------------------------------------------------+
//| Change enable stops and profit                                   |
//+------------------------------------------------------------------+
void ChangeEnableStopAndProfit(string el_name, int id, long lparam, double dparam, string sparam)
{
   int id_enable = ElementEnable;
   if(el_name == "EURUSDEnable")
   {
      GuiController::SendEvent("EURUSDProfit", id_enable, lparam, dparam, sparam);
      GuiController::SendEvent("EURUSDStop", id_enable, lparam, dparam, sparam);
   }
   else if(el_name == "GBPUSDEnable")
   {
      GuiController::SendEvent("GBPUSDProfit", id_enable, lparam, dparam, sparam);
      GuiController::SendEvent("GBPUSDStop", id_enable, lparam, dparam, sparam);
   }
}

En cuanto el EA reciba una notificación de que uno de los chekboxes ha sido marcado por el usuario, envía a GuiController un evento ElementEnable con el valor true. Si el usuario, al contrario, desmarca este elemento, se envía un evento ElementEnable con el valor false. Gracias a esta interacción entre el EA y el formulario con la ayuda de diferentes eventos, se crea un efecto de interactividad: el formulario empieza a cambiar la disponibilidad de los subelementos, dependiendo de la elección del usuario, aunque la propia lógica del control se encuentra directamente en el EA.


Botones de opción (Radio Button)

Los botones de opción representan un elemento gráfico simple para seleccionar un punto necesario desde los predefinidos:

Fig. 11. Botones de opción

Cuando el usuario cambia se elección, el EA recibe un evento sobre el cambio dos veces: la primera vez, del botón del que ha sido quitada la selección, el segundo evento llega del botón que ha sido seleccionado. Ambos eventos se rastrean usando el mismo identificador RadioButtonChange. Es el ejemplo de su uso:

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  else if(id == RadioButtonChange)
  {
      if(lparam == true)
         printf("Your have selected " + sparam);
      else
         printf("Your have deselected " + sparam);
  }
}

El parámetro lparam contiene la bandera que indica qué es lo que ha pasado con el botón: ha sido marcado (plaram = true) o la selección ha sido quitada (lparam = false). Al presionar los botones, el EA va a mostrar este tipo de mensajes en el terminal:

Your have deselected Expert
Your have selected Indicator
Your have deselected Indicator
Your have selected Script
...


Combo Box (lista desplegable)

La lista desplegable es uno de los elementos más comunes. Juntamente con CheckBox, encontró su aplicación tanto en el desarrollo Web, como en las aplicaciones móviles modernos. También es uno de los elementos de uso más frecuente en Windows:


Fig. 12. Lista desplegable y los elementos del menú disponibles

La lista se usa en dos modos principales. El primer modo, además de los elementos del menú existentes, permite al usuario introducir sus nuevos valores:

Fig. 13. Selección del instrumento con posibilidad de introducir su nuevo instrumento

El segundo modo ofrece al usuario sólo lo elementos del menú predefinidos, sin posibilidad de elegir opciones propias (Fig. 11). Existe el tercer modo que oculta los elementos del menú de selección, pero se usa de vez en cuando, por eso, no vamos a utilizarlo. 

Todos los modos de visualización de ComboBox se establecen usando su propiedad DropDownStyle. Normalmente, esta propiedad se establece una vez, en el momento del diseño de la interfaz gráfica, por eso, en GuiController no hay ningún evento que permita cambiar el tipo de ComboBox. Sin embargo, el controlador permite rastrear la selección del elemento de la lista, e incluso la introducción de un valor nuevo. Así, ComboBox soporta dos eventos: so propio ComboBoxChangeTextChange. Nuestro formulario de demostración contiene dos elementos ComboBox. El primero permite seleccionar una plataforma entre MetaTrader 4 y MetaTrader 5, el segundo selecciona un símbolo. Por defecto, el segundo elemento está bloqueado. Pero después de que el usuario elija la plataforma, se le hace disponible la selección de un instrumento financiero. El código que implementa esta funcionalidad se muestra a continuación: 

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == ComboBoxChange)
  {
     if(el_name == "comboBox1")
        //-- Desbloqueamos las lista de los símbolos en cuanto el usuario seleccione la plataforma:
        GuiController::SendEvent("comboBox2", ElementEnable, 1, 0.0, "");
     printf("ComboBox '" + el_name + "' was changed on " + sparam);
  }
}

Al comenzar a seleccionar los elementos desde las listas, el EA demo empieza a mostrar parámetros de la selección hechas por el usuario en el terminal:

ComboBox 'comboBox1' was changed on MetaTrader 5
ComboBox 'comboBox2' was changed on GBPUSD
ComboBox 'comboBox2' was changed on USDJPY
...


NumericUpDown (Ventana con enumeración numérica)

La ventana con enumeración numérica se utiliza con mucha frecuencia en los sistemas analíticos a los que pertenecen los paneles para el trading. Precisamente por eso, este elemento fue incluido en GuiController entre los primeros. La ventana con enumeración numérica permite definir un determinado valor controlando el tipo de la entrada (se puede definir sólo números). Al mismo tiempo, se puede ajustar el cambio del valor usando un mini scroll especial, así como, los dígitos del propio número.

Fig. 14. Ventana con enumeración numérica

GuiController soporta cuatro eventos para este tipo de controles:

  • NumericChange — recibe o envía un evento que contiene un valor numérico nuevo de la ventana;
  • NumericFormatChange — envía un evento que define la cantidad de dígitos del número (en la variable lparam) y el paso de su alteración (en la variable dparam);
  • NumericMaxChange — envía un evento que define el valor máximo posible del número;
  • NumericMinChange  — envía un evento que define el valor mínimo posible del número.

NumericUpDown interactúa con el usuario usando sólo el evento NumericChange. Cuando el usuario cambia el valor numérico en esta ventana, el EA recibe una notificación sobre ello a través de este evento. Aquí, la interacción con el usuario termina. No obstante, el propio EA puede configurar la ventana, colocando para ella los parámetros más importantes: número de dígitos, paso de alteración, valores máximos y mínimos permitidos. Todos estos parámetros dependen de la lógica del EA y de los tipos de datos con los que trabaja. Por eso, no hay posibilidad de establecerlos directamente en los ajustes del formularios, tienen que definirse en el momento de la inicialización del programa.

El EA de prueba incluye un pequeño ejemplo demo que muestra los principios de trabajo con NumericUpDown. Mostramos el código de la inicialización del formulario en la imagen 13. 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   if(ElementType != WINFORM_HIDE)
      EventSetMillisecondTimer(100);
   else
      EventSetMillisecondTimer(1000);
   switch(ElementType)
   {
      ...
      case WINFORM_NUMERIC:
      {
         GuiController::ShowForm(assembly, "NumericForm");
         double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
         double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
         double price_step = NormalizeDouble(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits());
         long digits = (long)Digits();
         GuiController::SendEvent("NumericForm", TextChange, 0, 0.0, "NumericForm (" + Symbol() + ")");
         NumericSet("StopLoss", Digits(), ask, (double)LONG_MAX, 0.0, price_step);
         NumericSet("TakeProfit", Digits(), bid, (double)LONG_MAX, 0.0, price_step);
         break;
      }
      ...
   }
   return(INIT_SUCCEEDED);
}

Como se puede observar, en el momento de la inicialización, el EA recibe los datos sobre el instrumento actual: sus precios Ask y Bid, número de dígitos y el paso del precio, y luego, establece estos parámetros para los controles del formulario NumericUpDown usando la función auxiliar especial NumericSet. Usamos su código:

//+------------------------------------------------------------------+
//| Set NumericUpDownParameter                                       |
//| name - name of NumericUpDown element                             |
//| digits - digits of symbol                                        |
//| init - init double value                                         |
//| max - max value                                                  |
//| min - min value                                                  |
//| step - step of change                                            |
//+------------------------------------------------------------------+
void NumericSet(string name, long digits, double init, double max, double min, double step)
{
   int id_foramt_change = NumericFormatChange;
   int id_change = NumericChange;
   int id_max = NumericMaxChange;
   int id_min = NumericMinChange;
   long lparam = 0;
   double dparam = 0.0;
   string sparam = "";
   GuiController::SendEvent(name, id_max, lparam, max, sparam);
   GuiController::SendEvent(name, id_min, lparam, min, sparam);
   GuiController::SendEvent(name, id_change, lparam, init, sparam);
   GuiController::SendEvent(name, id_foramt_change, digits, step, sparam);
}

Este código va a funcionar de forma adaptable. Dependiendo del símbolo en el que será iniciado, veremos un formato de precios diferente:


Fig. 15 Formatos propios del precio para cada instrumento financiero

DataTimePicker (Ventana para seleccionar la fecha)

Este control parece en su concepto a NumericUpDown con la única diferencia de que permite seleccionar las fechas de manera segura, en vez de los números:

Fig. 16. Selección de la fecha/hora exacta en el control DataTimePicker

La interacción con DataTimePicker es más fácil que en caso con el elemento NumericUpDown. Eso se debe al hecho de que el formato de la fecha es más o menos universal, a diferencia del formato del número que depende del entorno comercial actual del EA. Se puede definirlo en el momento del diseño del formulario y no cambiarlo más. Por eso, DataTimePicker soporta solamente el único evento DateTimePickerChange, que envía y recibe la fecha exacta a través del parámetro lparam. Mostramos un ejemplo del uso de este control:

for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  if(id == DateTimePickerChange)
     printf("User set new datetime value: " + TimeToString((datetime)lparam));
}

Al iniciar el EA demo y seleccionar DateTimePicker como elemento de demostración, aparece una ventana como en la imagen 15. Si empezamos a cambiar la fecha y la hora de maneras diferentes, el EA comenzará a reaccionar a estos eventos, mostrando un valor nuevo de la fecha y la hora en el log:

User set a new datetime value: 2019.05.16 14:21
User set a new datetime value: 2019.05.16 10:21
User set a new datetime value: 2021.05.16 10:21

No obstante, la interacción con el elemento es un poco más complicada que puede parecer. Es que el formato de la hora en MQL y en C# es diferente. MQL usa el formato de la fecha/hora POSIX simplificado, con resolución de 1 segundo y la fecha mínima posible 1970.01.01, mientras que C# usa un formato más avanzado, con resolución de 100 nanosegundos. De esta manera, para poder interactuar con sistemas diferentes, es necesario escribir un convector de tiempo que transforma un sistema de cálculo en otro. Este convector se usa en GuiController. Está implementado como una clase estática pública MtConverter:

/// <summary>
/// System Converter MetaTrader - C# 
/// </summary>
public static class MtConverter
{
    /// <summary>
    /// Convert C# DateTime format to MQL (POSIX) DateTime format.
    /// </summary>
    /// <param name="date_time"></param>
    /// <returns></returns>
    public static long ToMqlDateTime(DateTime date_time)
    {
        DateTime tiks_1970 = new DateTime(1970, 01, 01);
        if (date_time < tiks_1970)
            return 0;
        TimeSpan time_delta = date_time - tiks_1970;
        return (long)Math.Floor(time_delta.TotalSeconds);
    }
    /// <summary>
    /// Convert MQL (Posix) time format to sharp DateTime value.
    /// </summary>
    /// <param name="mql_time">MQL datetime as tiks</param>
    /// <returns></returns>
    public static DateTime ToSharpDateTime(long mql_time)
    {
        DateTime tiks_1970 = new DateTime(1970, 01, 01);
        if (mql_time <= 0 || mql_time > int.MaxValue)
            return tiks_1970;
        TimeSpan time_delta = new TimeSpan(0, 0, (int)mql_time);
        DateTime sharp_time = tiks_1970 + time_delta;
        return sharp_time;
    }
}

Actualmente, se compone solamente de dos métodos: el primero, ToMqlDateTime, convierte los valores de tiempo DateTime en el formato MQL. El segundo método hace absolutamente lo contrario, es decir, convierte el valor de tiempo MQL en la estructura C# DateTime. Como los tipos datetime (mql) y DateTime(C#) son incompatibles, la conversión se realiza a través del tipo común long, siendo éste el mismo para todos los sistemas. Así, al recibir el evento DateTimePickerChange, basta con convertir explícitamente el parámetro Iparam en datetime para obtener un valor de tiempo correcto:

//-- Convertimos explícitamente el valor long en datetime. Es absolutamente seguro
printf("User set new datetime value: " + TimeToString((datetime)lparam));

De la misma manera, no sólo podemos obtener nuevos valores, sino también definirlos personalmente. Por ejemplo, para definir la hora actual del terminal, se puede usar el siguiente comando:

GuiController::SendEvent("DateTimePicker", DateTimePickerChange, ((long)TimeCurrent()), 0.0, "");


ElementHide y ElementEnable - ocultar y desactivar un elemento aleatorio

Existen eventos universales a través de los cuales se puede controlar cualquier elemento de WinForms. ElementHide y ElementEnable pertenecen a este tipo de eventos. Para ocultar un elemento, hay que usar ElementHide, para eso, basta con escribir:

GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");

Donde "HideGroup" es el nombre del elemento a ocultar. Para mostrar este elemento, respectivamente, hay que llamar a:

GuiController::SendEvent("HideGroup", ElementHide, false, 0.0, "");

Preste atención al nombre del elemento usado. Resulta que el mismo elemento en WindowsForm puede contener los elementos internos. Eso se llama el anidamiento de elementos. Esta organización permite gestionar todos los elementos a nivel del grupo. En el ejemplo demo, se usa el cuadro (text box) que incluye la entrada label. El cuadro desaparece (con todos los elementos dentro) con cierta periodicidad, y luego vuelve a aparecer:

 

Fig. 17. Ocultando un elemento gráfico aleatorio del EA

Además, se puede hacer que cada elemento no esté disponible, así, no se oculta, pero es imposible trabajar con él. Es una opción útil que permite diseñar unas interfaces más complejas y comprensibles intuitivamente. Ya hemos mencionado el trabajo con este evento en la descripción de las banderas.  Repetimos que podemos hacer un elemento inaccesible enviando el siguiente evento:

GuiController::SendEvent("element", ElementEnable, false, 0.0, "");

Si sustituimos la bandera false por true, ele elemento se activará de nuevo:

GuiController::SendEvent("element", ElementEnable, true, 0.0, "");


AddItem - añadir subelementos

Algunos elementos pueden incluir otros elementos. Muchas veces, el contenido de estos subelementos se desconoce hasta el inicio de la ejecución del programa. Un ejemplo: supongamos que necesitamos mostrar una lista de instrumentos comerciales para que el usuario pueda seleccionar el necesario. Para eso, sería lógico usar ComboBox:


Fig. 18. Lista de símbolos predefinidos

Sin embargo, no se puede introducir los símbolos de antemano, en la fase de la compilación del formulario gráfico, ya que las listas de los instrumentos disponibles pueden ser diferentes de cada bróker. Por eso, es necesario formar el contenido de este tipo de forma dinámica. Para este propósito, se usa el comando AddItem. La manera más simple de hacer eso, es listar todos los símbolos disponibles en MarketWatch y añadirlos como elementos del menú:

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
   EventSetMillisecondTimer(100);
   GuiController::ShowForm(assembly, "SendOrderForm");
   for(int i = 0; i < SymbolsTotal(true); i++)
   {
      GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true));
   }
   return(INIT_SUCCEEDED);
}

Como se puede observar, es muy fácil usar este programa. Aunque el comando es universal, no todos los elementos permiten añadir otros elementos dentro. En este momento, GuiController soporta este evento sólo para ComboBox. Aunque eso es suficiente para diseñar las interfaces gráficas eficaces. Tal vez, esta lista sea ampliada en el futuro.


Exception — evento para recibir excepciones

No siempre es posible escribir un programa sin errores. Además, pueden surgir las situaciones cuando algo falla y la ejecución del programa se realiza según un escenario imprevisto. En estas situaciones, necesitamos un feedback con posibilidad de obtener la información sobre lo que ha pasado. Para este tipo de interacción, tenemos un evento especial Exception. El propio GuiController lo genera y lo envía al EA MQL. GuiController puede crear mensajes sobre los errores en dos casos:

  1. En caso de una intersección de una excepción de sistema;
  2. Si el evento enviado desde el EA no soporta el elemento elegido para eso, o el propio evento tiene valores inválidos.

Analizaremos estas opciones por orden. Para demostrar la primera opción, volvemos a nuestro código demo, es decir, a la versión que muestra los elementos NumericUpDown. En esta versión, se invoca la función especial NumericSet, que establece los intervalos de trabajo de los elementos NumericUpDown:

void NumericSet(string name, long digits, double init, double max, double min, double step)
{
   int id_foramt_change = NumericFormatChange;
   int id_change = NumericChange;
   int id_max = NumericMaxChange;
   int id_min = NumericMinChange;
   long lparam = 0;
   double dparam = 0.0;
   string sparam = "";
   // GuiController::SendEvent(name, id_max, lparam, max, sparam);
   GuiController::SendEvent(name, id_min, lparam, min, sparam);
   GuiController::SendEvent(name, id_change, lparam, init, sparam);
   GuiController::SendEvent(name, id_foramt_change, digits, step, sparam);
}

Esta función fue ligeramente modificada. Ahora, el valor máximo para el elemento se define (la línea correspondiente está comentada). Si ejecutamos este formulario en el gráfico del oro después de la compilación, el formulario de repente deja de mostrar los precios actuales:

Fig. 19. Valores cero en el formulario de definición de precios

¿Qué ha pasado? Para determinar eso, existe un sistema de excepciones. Se puede recibir cada excepción de este tipo, como cualquier otro mensaje, a través de GuiController::GetEvent:

//-- get new events by timer
for(static int i = 0; i < GuiController::EventsTotal(); i++)
{
  int id;
  string el_name;
  long lparam;
  double dparam;
  string sparam;
  GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
  ...
  if(id == Exception)
     printf("Unexpected exception: " + sparam);
  
}

La visualización del mensaje puede decir mucho:

Unexpected exception: El valor '1291,32' es inválido para 'Value'. 'Value' debe estar en el intervalo de 'Minimum' a 'Maximum'.
Nombre del parámetro: Value
Unexpected exception: El valor '1291,06' es inválido para 'Value'. 'Value' debe estar en el intervalo de 'Minimum' a 'Maximum'.
Nombre del parámetro: Value

Es que NumericUpDown, por defecto, tiene el rango de trabajo de 0 a 100. Por tanto, al tratar de establecer el precio actual del oro (1292 dólares por onza troy del oro), surge un error. Para evitarlo, es necesario expandir el rango de valores permitidos programáticamente. Para eso, se puede usar el evento NumericMaxChange, de lo que se encarga la línea comentada en la función NumericSet.

La segunda opción de llamar a la excepción es posible si enviamos un evento incorrecto. Por ejemplo, podemos intentar enviar un evento con una dirección inexistente:

GuiController::SendEvent("empty", ElementEnable, 0, 0.0, "");

Como respuesta obtenemos:

Unexpected exception: SendEvent: element with name 'empty' not find

También se puede intentar enviar un evento no soportados por el elemento. Por ejemplo, intentamos añadir un texto en campo de entrada del nivel de Stop Loss (tipop del elemento NumericUpDown):

GuiController::SendEvent("StopLoss", AddItem, 0, 0.0, "New Text");

La respuesta no será menos concisa:

Unexpected exception: Element 'StopLos' doesn't support 'Add Item' event

El sistema de excepciones ofrece una ayuda inapreciable en la creación de las interfaces gráficas complejas. Los errores en estas aplicaciones son inevitables. En función de la rapidez con la que el desorrallador puede comprender estos errores, va a depender la velocidad y conveniencia del desarrollo.


Tabla de elementos gráficos y eventos disponibles

Para trabajar con GuiController, sería conveniente sistematizar los elementos gráficos soportados. Para este propósito, creamos una tabla con información resumida sobre estos elementos y los modos de su uso en GuiController. En la columna «Ejemplo de uso», mostramos un breve ejemplo del código en el cual se muestre el trabajo con este elemento de un programa MQL. El código en cuestión debe ser considerado como una parte del patrón general del trabajo con los eventos. Así, por ejemplo, es el código del primer ejemplo con MessageBox:

string msg = "!!!|The operation was cancelled|Critical Error";
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);

Debe ser considerado en el siguiente contexto:

void OnTimer
{
   //...
   //-- get new events by timer
   for(static int i = 0; i < GuiController::EventsTotal(); i++)
   {
     int id;
     string el_name;
     long lparam;
     double dparam;
     string sparam;
     GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
     if(id == MessageBox)
     {
        string msg = "!!!|The operation was cancelled|Critical Error";
        GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);
     }
  }
}

De la misma forma, hay que aplicar este patrón a otros ejemplos del uso.

Elemento gráfico Nombre de elemento o evento Identificadores de eventos clave Ejemplo de uso

MessageBox MessageBox
string msg = "!!!|The operation was cancelled|Critical Error";
GuiController::SendEvent("ButtonForm", MessageBox, LockControl, OK, msg);



Tabs TabIndexChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Selecet new tab. Index: " + (string)lparam + " Name: " + sparam);



CheckBox CheckBoxChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Checked " + sparam + " " + lparam);


RadioButton RadioButtonChange
            
            
            
            
            
            

GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); if(lparam == true)    printf("Your have selected " + sparam); else    printf("Your have deselected " + sparam);




ComboBox ComboBoxChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("ComboBox '" + el_name + "' was changed on " + sparam);


NumericUpDown NumericChange
NumericFormatChange
NumericMaxChange
NumericMinChange
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Numeric '" + el_name + "' was changed, new value: " + DoubleToString(dparam, 4));



DateTimePicker DateTimePickerChange
            
            
            
            
            
            

GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam); printf("User set new datetime value: " + TimeToString((datetime)lparam));



 

Vertical Scroll  ScrollChange  
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Vertical Scroll has new value: " + (string)lparam);

 

 TextBox TextChange   
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("new value entered: " + sparam);

 

 Button  ClickOnElement  
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Button " + sparam + " is pressed");

  Label TextChange   
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Label has new text: " + sparam);

Además de los elementos gráficos, GuiController soporta los eventos universales para trabajar con ellos. También sistematizamos estos eventos en forma de una tabla semejante:

Evento Descripción Ejemplo de uso
ElementHide Ocultar o recuperar elemento
GuiController::SendEvent("HideGroup", ElementHide, true, 0.0, "");
ElementEnable Activa o desactiva elemento
GuiController::SendEvent("HideGroup", ElementEnable, true, 0.0, "");
AddItem Añade nuevo subelemento al elemento  
GuiController::ShowForm(assembly, "SendOrderForm");
for(int i = 0; i < SymbolsTotal(true); i++)
   GuiController::SendEvent("SymbolComboBox", AddItem, 0, 0, SymbolName(i, true));
Exeption  Recibe una excepción llamada dentro de CLR  
GuiController::GetEvent(i, el_name, id, lparam, dparam, sparam);
printf("Unexpected exception: " + sparam);



Conclusiones

Hemos analizado los principales elementos gráficos de WindowsForms, así como, los ejemplos de interacción con ellos. Estos elementos son pocos, pero representan una «base» de cualquiera aplicación gráfica. Y aunque entre ellos no hay otro elemento sumamente importante en el campo del trading, es decir, las tablas, no obstante, ya se puede diseñar aplicaciones gráficas bastante funcionales basándose en ellos.

La última versión GuiController se adjunta al artículo. Además, puede clonar esta versión desde el sistema del repositorio GitHub. Recordaré que la versión de esta biblioteca se encuentra en:  https://github.com/PublicMqlProjects/MtGuiController.git. Además, puede clonar el proyecto del formulario de demostración. Para eso, siga el enlace  https://github.com/PublicMqlProjects/GuiControllerElementsDemo.git  En la primera parte del artículo, se explica cómo se puede obtener la última versión de la biblioteca a través del sistema de control de versiones. Los códigos fuente de todos los proyectos se adjuntan. Son tres: el EA que llama al formulario con elementos, conjunto de formularios en el ensamblado DemoForm.exe y GuiController en versión compilada (Source\MQL5\Libraries\GuiController.dll) y en forma de códigos fuente (Source\Sharp\GuiController). También hay que prestar atención en el hecho de que al EA demo es necesario indicarle la ruta absoluta al formulario a ejecutar. En su ordenador, ella será diferente de lo que se especifica en el parámetro assemble del EA, por eso, hay que cambiarla por la ruta real.

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

Archivos adjuntos |
Source.zip (50.14 KB)
Estimación del índice de fractalidad, exponente de Hurst y posibilidad de predecir series temporales financieras Estimación del índice de fractalidad, exponente de Hurst y posibilidad de predecir series temporales financieras
La búsqueda y el estudio del comportamiento fractal de los datos financieros supone que, tras un comportamiento aparentemente caótico de la series temporales económicas, se ocultan y operan unos mecanismos estables del comportamiento colectivo de los participantes. En la bolsa, estos mecanismos pueden llevar a la aparición de una dinámica de precios que determina y describe las propiedades específica de las series de precios. En el trading, sería interesante tener indicadores que pudieran estimar los parámetros de la fractilidad de manera estable y eficaz, en una escala y un intervalo de tiempo que fuesen útiles en la práctica.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte VI): Eventos en la cuenta con compensación Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte VI): Eventos en la cuenta con compensación
En anteriores artículos comenzamos a crear una gran biblioteca multiplataforma cuyo objetivo es simplificar la escritura de programas para las plataformas MetaTrader 5 y MetaTrader 4. En la quinta parte, hemos creado las clases de los eventos comerciales y la colección de eventos desde donde se envían los eventos a la objeto de la biblioteca Engine y al gráfico del programa de control. En esta parte de la descripción, vamos a añadir la posibilidad de trabajar con la biblioteca en las cuentas de tipo compensación.
Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte VII): Eventos de activación de órdenes StopLimit, preparación de la funcionalidad para los eventos de modificación de órdenes y posiciones Biblioteca para el desarrollo rápido y sencillo de programas para MetaTrader (Parte VII): Eventos de activación de órdenes StopLimit, preparación de la funcionalidad para los eventos de modificación de órdenes y posiciones
En anteriores artículos comenzamos a crear una gran biblioteca multiplataforma cuyo objetivo es simplificar la creación de programas para las plataformas MetaTrader 5 y MetaTrader 4. En la sexta parte, enseñamos a la biblioteca a trabajar con posiciones en las cuentas de compensación. En esta parte, implementaremos el seguimiento de los eventos de activación de órdenes StopLimit y prepararemos la funcionalidad necesaria para monitorear la modificación de órdenes y posiciones.
Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales Implementando OLAP en la negociación (Parte 2): Visualización de los resultados del análisis interactivo de los datos multidimensionales
En este artículo, se consideran diversos aspectos del desarrollo de la interfaz gráfica interactiva de un programa MQL diseñado para el procesamiento analítico en línea (OLAP) del historial de la cuenta y de los informes comerciales. Para obtener un resultado visual, se usan las ventanas maximizadas y de escala, una disposición adaptable de los controles «de goma» y un nuevo control para mostrar diagramas. A base de eso, fue implementado GUI con una selección de indicadores a lo largo de los ejes de coordenadas, funciones agregadas, tipos de los gráficos y ordenaciones.