English Русский 中文 Deutsch 日本語 Português
preview
Cómo hacer el gráfico más interesante: Adicionando un fondo de pantalla

Cómo hacer el gráfico más interesante: Adicionando un fondo de pantalla

MetaTrader 5Ejemplos | 12 abril 2022, 14:20
1 073 6
Daniel Jose
Daniel Jose

Introducción

Muchos terminales de trabajo contienen alguna imagen representativa y que muestra algo del usuario, estas imágenes hacen que el entorno de trabajo sea más bonito y alegre, la gente siempre intenta elegir las mejores y más bonitas imágenes para ponerlas como fondo de pantalla, pero cuando abrimos la plataforma de comercio, la cosa se vuelve aburrida y sosa, donde lo único que tenemos son representaciones gráficas de datos numéricos.

 


Podemos mirar una imagen durante mucho tiempo sin cansarnos, pero mirar un gráfico numérico durante unos minutos es muy agotador, así que hagamos las cosas más interesantes, para que podamos mirar y analizar el gráfico mientras la imagen de fondo nos motiva y nos recuerda algo bueno...


Planificación

Para empezar, debemos definir una cosa que influirá en el funcionamiento de todo el proyecto, la cosa en cuestión es: ¿Queremos cambiar el fondo del gráfico de vez en cuando, o vamos a utilizar una sola imagen para toda la existencia del programa, teniendo una sola imagen para todos los gráficos? Bueno, a mí me gusta poner diferentes imágenes en cada gráfico, algo que pueda representar el tipo de activo con el que estoy operando, o algo que indique lo que debo buscar en ese activo en un momento dado, por eso el archivo compilado no tendrá ninguna imagen interna, así que podemos seleccionar cualquier imagen después.

Una vez definido esto, tenemos que entender otra cosa: ¿Dónde deben estar nuestras imágenes? Pues bien, MetaTrader 5 tiene una estructura de directorios que debemos utilizar para acceder a las cosas, no podemos utilizar el árbol de directorios fuera de estos límites, y saber utilizar esta estructura es primordial para poder acceder a las imágenes posteriormente. Como la intención es organizar y mantener esta organización en el tiempo, vamos a crear un directorio dentro del directorio FILES, y lo llamaremos WALLPAPERS. La razón para hacerlo es que cuando accedamos a las imágenes no podremos salir del árbol cuya raíz es el directorio MQL5...

Pero, ¿por qué no poner los archivos en la carpeta IMAGES? Sería posible hacerlo, pero entonces tendríamos que navegar por el árbol, lo que sería una tarea innecesaria, complicando la lógica del programa, pero como mi intención es siempre mantener las cosas lo más simples posible, vamos a utilizar lo que nos ofrece MetaTrader 5. Así que nuestra estructura será la que se muestra a continuación:




Una vez hecho esto, añadiremos las imágenes como se muestra a continuación, es decir, separamos las imágenes del logotipo de las imágenes genéricas de fondo. Esto es importante para mantener las cosas lo más organizadas posible, ya que las imágenes que se van a utilizar como logotipos pueden tener un número muy grande, si nos fijamos en varios activos.



Observen que es muy sencillo, pongan todas las imágenes que deseen, y esto no estorbará para nada al programa. Ahora un detalle, noten que las imágenes están en tipo BITMAP, y tienen que estar en tipo 24 bits o 32 bits, y esto es porque esos formatos son más fáciles de leer, el propio MetaTrader 5 puede leer esos formatos por defecto, así que mantuve las cosas así, pero nada nos impide usar otro tipo, siempre y cuando programemos la rutina de lectura de forma que tengamos una imagen BITMAP al final, es decir, es más sencillo usar un editor de imágenes y convertir la cosa al estándar de mapa de bits de 24 o 32 bits, que crear una rutina sólo para hacer esto. Los archivos del directorio LOGOS siguen los mismos principios, con algunas excepciones que veremos pronto.

Teniendo esto en cuenta, comencemos la parte de la codificación. El código sigue los preceptos de la programación orientada a objetos (POO), por lo que puede transportarlo fácilmente a un script o indicador, si lo prefieren, además de poder aislarlo cuando sea necesario.


El paso a paso

El código comienza con algunas definiciones:´

//+------------------------------------------------------------------+
enum eType {IMAGEM, LOGO, COR};
//+------------------------------------------------------------------+
input char     user01 = 30;               //Transparencia ( 0 a 100 )
input string   user02 = "WallPaper_01";   //Nome do arquivo
input eType    user03 = IMAGEM;           //Tipo de fundo do grafico
//+------------------------------------------------------------------+

Aquí indicamos lo que vamos a hacer, la enumeración eType indica cuál sería el fondo gráfico, puede ser una IMAGEN, un LOGO, o un COLOR, no es necesario ir más allá. La entrada USER02 indica el nombre del archivo que se utilizará como fondo, ya que el tipo IMAGEN está seleccionado en la entrada USER03. Ya USER01 indica el nivel de transparencia de nuestra imagen de fondo, ya que en algunos casos puede entorpecer la perfecta visualización de los datos en el gráfico, entonces cambiamos la transparencia para minimizar este efecto, el valor es sencillo de entender, va de 0% a 100% de transparencia, cuanto mayor sea el valor más transparente será la imagen de fondo.




Las funciones que realmente debes añadir a tu programa son las siguientes:

Función Parámetros Dónde declarar la función  Resultado
Init(string szName, char cView) Nombre del archivo y nivel de transparencia deseado Como primera función del código OnInit Carga el archivo BITMAP especificado y lo muestra con la transparencia indicada
Init(string szName)  Sólo se requiere el archivo Como primera función del código OnInit Carga el archivo BITMAP especificado sin ningún grado de transparencia
Resize(void) No se requieren parámetros En el código OnChartEvent, en el evento CHARTEVENT_CHART_CHANGE Redimensiona la imagen adecuadamente en el gráfico

Así que vamos a ver cómo utilizar estas funciones en el código principal, comenzando por la inicialización de la clase, que tendrá lugar como se muestra a continuación, nótese que en este caso el usuario puede indicar un nivel de transparencia, y para que sea correcto debemos corregir el valor restándolo de 100.

int OnInit()
  {
   if(user03 != COR)
      WallPaper.Init(user03 == IMAGEM ? "WallPapers\\" + user02 : "WallPapers\\Logos\\" + _Symbol, (char)(100 - user01));
   return INIT_SUCCEEDED;
  }

Fíjense que si usamos el modo COLOR no aparecerá ninguna imagen, pero fíjense bien en el operador triple. Cuando seleccionemos una imagen, apuntará al directorio WALLPAPER dentro del árbol FILES y cuando sea un LOGO apuntará al lugar correcto, pero vean con atención que el nombre del archivo de la imagen en caso de ser un logo debe ser el nombre del símbolo, de lo contrario se generará un error. En el caso de utilizar series continuas la cosa acaba ahí, pero puede ser que estemos utilizando un activo con fecha de caducidad, en este caso habrá que añadir una pequeña rutina para separar la parte del nombre que diferencia una serie actual de una caducada, pero el simple hecho de renombrar el archivo de la imagen para que el nombre sea el de la serie actual, ya solucionará el problema, para los que operan utilizando órdenes cruzadas, quizás sea interesante construir esta rutina para ajustar el nombre del símbolo.

Bueno, la siguiente rutina que aún merece ser destacada es:

void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE)
      WallPaper.Resize();
  }

Todos los códigos son siempre muy cortos, no me gusta complicar demasiado, ya que esto dificulta las mejoras y los cambios en el sistema, también trate de hacer de esto una regla cuando se crean las cosas, la rutina de arriba garantizará que cualquier cambio en las dimensiones de la carta, llame a la rutina responsable de cambiar la escala de la imagen, manteniendo las cosas siempre agradable a la vista y la imagen se renderiza completamente.

El código de nuestra clase tiene las siguientes características:

Función Parámetros   Resultado 
MsgError(const eErr err, int fp) Tipo de error y descriptor del archivo Cierra el archivo y muestra el mensaje de error correspondiente
MsgError(const eErr err) Tipo de error Muestra el mensaje sobre el error correspondiente
LoadBitmap(const string szFileName, uint &data[], int &width, int &height) Nombre del archivo y punteros de datos Carga el archivo deseado y devuelve sus datos en data[] más sus dimensiones en píxeles
~C_WallPaper() No se requieren parámetros Garantiza el cierre de la clase objeto
Init(const string szName, const char cView) Nombre del archivo y nivel de transparencia Inicializa toda la clase correctamente
Init(const string szName) Nombre del archivo Inicializa toda la clase correctamente
Destroy(void) No se requieren parámetros Termina la clase de manera apropiada
Resize(void)  No se requieren parámetros Realiza el redimensionamiento de la imagen correctamente

Para no hacer todo el código un lío, concentré el manejo de errores en una sola rutina que se puede ver a continuación, lo único que hace es enviar un mensaje al usuario indicando que algo está mal, esto facilita las cosas, ya que en caso de traducción a otro idioma lo único que tenemos que hacer es cambiar los mensajes en una sola rutina, y no estar buscando donde se presenta cada uno de los mensajes.

   bool              MsgError(const eErr err, int fp = 0)
     {
      string sz0;
      switch(err)
        {
         case FILE_NOT_FOUND  :
            sz0 = "Archivo no encontrado";
            break;
         case FAILED_READ     :
            sz0 = "Error de lectura";
            break;
         case FAILED_ALLOC    :
            sz0 = "Error de memoria";
            break;
         case FAILED_CREATE   :
            sz0 = "Error en la creación del recurso interno";
            break;
        };
      MessageBox(sz0, "ADVERTENCIA", MB_OK);
      if(fp > 0)
         FileClose(fp);
      return false;
     }

La función que se muestra a continuación leerá un archivo y lo cargará en memoria, la única información que necesitamos es el nombre del archivo, los demás datos serán llenados por la función, al final tendremos las dimensiones de la imagen y la imagen en sí, pero en el formato BITMAP, es importante notar esto, porque aunque hay varios formatos, al final el resultado siempre será un BITMAP, sólo la forma en que se compacta es lo que diferencia un formato de otro.

   bool              LoadBitmap(const string szFileName, uint &data[], int &width, int &height)
     {
      struct BitmapHeader
        {
         ushort      type;
         uint        size;
         uint        reserv;
         uint        offbits;
         uint        imgSSize;
         uint        imgWidth;
         uint        imgHeight;
         ushort      imgPlanes;
         ushort      imgBitCount;
         uint        imgCompression;
         uint        imgSizeImage;
         uint        imgXPelsPerMeter;
         uint        imgYPelsPerMeter;
         uint        imgClrUsed;
         uint        imgClrImportant;
        } Header;
      int fp;
      bool noAlpha, noFlip;
      uint imgSize;

      if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE)
         return MsgError(FILE_NOT_FOUND);
      if(FileReadStruct(fp, Header) != sizeof(Header))
         return MsgError(FAILED_READ, fp);
      width = (int)Header.imgWidth;
      height = (int)Header.imgHeight;
      if(noFlip = (height < 0))
         height = -height;
      if(Header.imgBitCount == 32)
        {
         uint tmp[];
         noAlpha = true;
         imgSize = FileReadArray(fp, data);
         if(!noFlip)
            for(int c0 = 0; c0 < height / 2; c0++)
              {
               ArrayCopy(tmp, data, 0, width * c0, width);
               ArrayCopy(data, data, width * c0, width * (height - c0 - 1), width);
               ArrayCopy(data, tmp, width * (height - c0 - 1), 0, width);
              }
         for(uint c0 = 0; (c0 < imgSize && noAlpha); c0++)
            if(uchar(data[c0] >> 24) != 0)
               noAlpha = false;
         if(noAlpha)
            for(uint c0 = 0; c0 < imgSize; c0++)
               data[c0] |= 0xFF000000;
        }
      else
        {
         int byteWidth;
         uchar tmp[];
         byteWidth = width * 3;
         byteWidth = (byteWidth + 3) & ~3;
         if(ArrayResize(data, width * height) != -1)
            for(int c0 = 0; c0 < height; c0++)
              {
               if(FileReadArray(fp, tmp, 0, byteWidth) != byteWidth)
                  return MsgError(FAILED_READ, fp);
               else
                  for(int j = 0, k = 0, p = width * (height - c0 - 1); j < width; j++, k+=3, p++)
                     data[p] = 0xFF000000 | (tmp[k+2] << 16) | (tmp[k + 1] << 8) | tmp[k];
              }
        }
      FileClose(fp);
      return true;
     }

Vean la siguiente línea en el código:

      if((fp = FileOpen(szFileName + ".bmp", FILE_READ | FILE_BIN)) == INVALID_HANDLE)

Observemos que en este punto se indica la extensión del archivo, es decir, no debemos indicar la extensión en el momento en que vamos a decir cuál será la imagen, porque si lo hacemos se generará un error de archivo no encontrado. Todo el resto de la función es bastante básico, primero leerá la cabecera del archivo, y comprobará si es un BITMAP de 32 o 24 bits, luego leerá la imagen de la forma correcta, ya que una imagen de 32 bits tiene una estructura interna un poco diferente a la de 24 bits.

La siguiente función inicializará todos los datos para que nuestro mapa de bits se muestre en pantalla. Una cosa que hay que notar es que, durante esta función, transformaremos el archivo de mapa de bits en un recurso del programa, esto es necesario, ya que más tarde vincularemos este recurso a un objeto, y es exactamente este objeto el que se mostrará en la pantalla, pero no como un objeto, sino como un recurso. Parece complicado entender por qué lo hacemos así, pero es algo que nos permite crear varios recursos del mismo tipo y luego vincularlos a un único objeto que se utilizará para mostrar algo. Si pusiéramos un único recurso bien definido en el programa, simplemente lo definiríamos como un recurso interno y compilaríamos el archivo, pero esto nos impediría cambiar el recurso sin tener que recompilar el código fuente, pero al crear un recurso dinámicamente, podemos indicar qué recurso queremos utilizar.

   bool              Init(const string szName, const char cView = 100, const int iSub = 0)
     {
      double dValue = ((cView > 100 ? 100 : (cView < 0 ? 0 : cView)) * 2.55) / 255.0;
      m_Id = ChartID();
      if(!LoadBitmap(szName, m_BMP, m_MemWidthBMP, m_MemHeightBMP))
         return false;
      Destroy();
      m_Height = m_MemHeightBMP;
      m_Width = m_MemWidthBMP;
      if(ArrayResize(m_Pixels, (m_MemSizeArr = m_Height * m_Width)) < 0)
         return MsgError(FAILED_ALLOC);
      m_szRcName = "::" + szName + (string)(GetTickCount64() + MathRand());
      if(!ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE))
         return MsgError(FAILED_CREATE);
      if(!ObjectCreate(m_Id, (m_szObjName = szName), OBJ_BITMAP_LABEL, iSub, 0, 0))
         return MsgError(FAILED_CREATE);
      ObjectSetInteger(m_Id, m_szObjName, OBJPROP_XDISTANCE, 0);
      ObjectSetInteger(m_Id, m_szObjName, OBJPROP_YDISTANCE, 0);
      ObjectSetString(m_Id, m_szObjName, OBJPROP_BMPFILE, m_szRcName);
      ObjectSetInteger(m_Id, m_szObjName, OBJPROP_BACK, true);
      for(uint i = 0; i < m_MemSizeArr; i++)
         m_BMP[i] = (uchar(double(m_BMP[i] >> 24) * dValue) << 24) | m_BMP[i] & 0x00FFFFFF;
      return true;
     }

Todo esto es muy bonito y aparentemente práctico, pero el objeto en sí no es capaz de cambiar el recurso, es decir, no es posible cambiar la forma en que un recurso funcionará o se presentará simplemente vinculándolo a un objeto. Esto a veces complica un poco las cosas, ya que la mayoría de las veces tenemos que codificar cómo se debe cambiar el recurso dentro del objeto.

Hasta este punto se podía presentar la imagen utilizando el siguiente código:

      if(ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE))
         ChartRedraw();

Pero el uso de este código no garantizará que la imagen se presente como se espera, salvo el hecho de que tenga las dimensiones exactas del gráfico, pero les aconsejo que usen imágenes de alta definición, las imágenes grandes se representan mejor y hace que la parte de los cálculos sea mucho más fácil, ahorrando así tiempo de procesamiento que puede ser crucial en varios escenarios, pero aun así seguimos teniendo el problema de que la imagen no se presente correctamente ya que el objeto no cambiará el recurso de manera que se ajuste a las especificaciones del objeto, por lo que tenemos que hacer algo para que el recurso se modele de manera correcta y así se pueda presentar usando el objeto. Las matemáticas involucradas en el caso de una imagen es algo que va desde el cálculo más simple, hasta cosas muy complicadas, pero como estamos haciendo algo que el tiempo de procesamiento es crucial, que es usar un gráfico de precios, no podemos permitirnos hacer demasiados cálculos, debemos hacer la cosa lo más simple y rápida posible, y esto nos lleva a que tendríamos que usar imágenes con dimensiones más grandes que nuestro gráfico, porque entonces lo único que tendríamos que calcular es la reducción, veamos cómo funcionaría esto.

Las relaciones matemáticas que representarán el gráfico anterior se pueden conseguir de la siguiente manera:


Atención al siguiente hecho, hemos conseguido que f(x) = f(y) y esto mantiene la relación o proporción de la imagen, esto también se conoce como "aspect ratio", es decir, la imagen se está modificando de forma completa, esto ampliará y reducirá las dimensiones, pero, ¿y si f(y) fuera independiente de f(x) qué pasaría con nuestra imagen? Bueno, se modificaría de forma no proporcional, asumiendo así cualquier forma. Pero aunque no tenemos problemas con la reducción, no ocurre lo mismo con la ampliación, es decir, si el valor de f(x) > 1,0 o f(y) > 1,0, tendremos una ampliación de la imagen y en este punto empiezan a surgir algunos problemas. El primer problema puede verse a continuación:


La razón de que esto ocurra es que la imagen está sufriendo el efecto que se puede notar mejor en la figura de abajo, fíjense bien que los espacios BLANCOS representan los espacios vacíos que aparecen en la imagen cuando ésta sufre el efecto de ampliación, y este hecho se registrará siempre cuando f(x) o f(y) sea mayor que 1,0, es decir, cuando sigamos la flecha roja. En el caso de la figura siguiente f(x) = f(y) = 2,0, es decir, estamos ampliando la imagen en 2x.


Hay varias formas de sortear este problema, una de ellas es la interpolación, esta debe producirse cuando se encuentra un bloque vacío, en este momento factorizaríamos y calcularíamos un color intermedio entre los utilizados, esto produce un efecto de suavizado a la vez que tenemos el relleno de los puntos vacíos, pero tenemos un problema que es la parte de cálculo, aunque la interpolación se produzca rápidamente, puede no ser la forma más adecuada para ser utilizada en gráficos como los producidos en MetaTrader 5, que son gráficos en tiempo real, aunque el redimensionamiento se haga pocas veces durante todo el tiempo que el gráfico esté en pantalla, ya que en la mayoría de las ocasiones las dimensiones del gráfico serán inferiores a las de la imagen, y en este caso, la f(x) y la f(y) serían iguales o inferiores a 1. 0, y la interpolación no aportaría ningún beneficio, pensando en utilizar imágenes de 1920 x 1080 (imagen FULL HD) en una pantalla con la misma dimensión, añadir el cálculo de la interpolación sólo provocaría un aumento en el tiempo de procesamiento sin beneficiar el resultado final.

Veamos a continuación como se haría el cálculo de la interpolación en una imagen que duplicará su tamaño, aparentemente la cosa sería muy rápida, pero hay que recordar que hay que hacerlo en un patrón de color de 32 bits, o ARGB, donde tenemos 4 bytes de 8 bits a calcular, el procesador gráfico tiene funciones que nos permiten hacer estos cálculos rápidamente, pero acceder a estas funciones a través de OpenCL puede no darnos ningún beneficio práctico, ya que tendríamos un retardo para poner y tomar la información del procesador gráfico, y este tiempo puede no aportarnos ningún beneficio de la velocidad del cálculo realizado por el sistema gráfico.


                             


Pensando en esto, asumo que una pequeña degradación de la imagen por el efecto del suavizado no sería para nada malo, ya que en la mayoría de las veces la f(x) o la f(y) no estarían por encima de 2, y esto ocurriría en un caso de usar una imagen FULL HD en una pantalla 4k, en este escenario el suavizado sería mínimo y difícil de notar. Así que en lugar de interpolar los puntos, prefiero arrastrar el punto al siguiente, rellenando rápidamente los valores vacíos, reduciendo así el coste computacional al máximo. La forma en que se hace esto se puede ver a continuación, ver que es bastante simple, y ya que estamos simplemente copiando los datos, podemos tratar los 32 bits en un solo paso, y esto sería tan rápido como lo que sería entregado por el sistema de procesamiento de gráficos.


                   


De esta forma llegamos a la rutina necesaria para que el redimensionamiento de la imagen sea lo más rápido posible.

void Resize(void)
{
        m_Height =(uint) ChartGetInteger(m_Id, CHART_HEIGHT_IN_PIXELS);
        m_Width = (uint) ChartGetInteger(m_Id, CHART_WIDTH_IN_PIXELS);
        double fx = (m_Width * 1.0) / m_MemWidthBMP;
        double fy = (m_Height * 1.0) / m_MemHeightBMP;
        uint pyi, pyf, pxi, pxf, tmp;

        ArrayResize(m_Pixels, m_Height * m_Width);
        ArrayInitialize(m_Pixels, 0x00FFFFFF);
        for (uint cy = 0, y = 0; cy < m_MemHeightBMP; cy++, y += m_MemWidthBMP)
        {
                pyf = (uint)(fy * cy) * m_Width;
                tmp = pyi = (uint)(fy * (cy - 1)) * m_Width;
                for (uint x = 0; x < m_MemWidthBMP; x++)
                {
                        pxf = (uint)(fx * x);
                        pxi = (uint)(fx * (x - 1));
                        m_Pixels[pxf + pyf] = m_BMP[x + y];
                        for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = m_BMP[x + y];
                }
                for (pyi += m_Width; pyi < pyf; pyi += m_Width) for (uint x = 0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp];
        }
        if (ResourceCreate(m_szRcName, m_Pixels, m_Width, m_Height, 0, 0, 0, COLOR_FORMAT_ARGB_NORMALIZE))
                ChartRedraw();
}   

La rutina tiene un bucle anidado, el bucle interior ejecutará la función f(x) y el bucle exterior la función f(y), cuando ejecutamos la función f(x) podemos producir bloques vacíos, esto se corrige en esta línea:

for (pxi++; pxi < pxf; pxi++) m_Pixels[pxi + pyf] = m_BMP[x + y];

Si por casualidad se produce una diferencia entre los valores de X, esta línea lo corregirá copiando el último valor de la imagen, como consecuencia tendremos la producción de un "aliasing", pero el coste computacional en estos casos sería mínimo, ya que este fragmento tendría un bucle interno ejecutándose durante un tiempo mínimo, esto cuando se ejecute, que no será siempre, si se quiere interpolar los datos evitando este efecto "aliasing", basta con modificar esta línea para crear los cálculos que se explicaron anteriormente.

Una vez calculada una línea completa, comprobamos f(y) para evitar que aparezcan bloques vacíos si f(y) es mayor que 1, y esto se consigue en esta línea:

for (pyi += m_Width; pyi < pyf; pyi += m_Width) for (uint x = 0; x < m_Width; x++) m_Pixels[x + pyi] = m_Pixels[x + tmp];

De nuevo esto producirá un "aliasing", pero se puede arreglar de la misma manera que se cambiaría el código de la línea anterior. El hecho de añadir el valor de la anchura de la nueva imagen se debe a que estamos copiando una línea ya tratada por el bucle encargado de tratar la f(x) de la nueva imagen, si la suma se hiciera con cualquier otro valor, la imagen se deformaría de forma extraña.


Conclusión

Espero que esta idea permita que sus gráficos sean más divertidos y agradables y que puedan ser observados durante horas y horas, ya que cuando la imagen de fondo resulte cansina y empalagosa, sólo hay que elegir otra, sin tener que recompilar nada, sólo eligiendo la nueva imagen que se utilizará como fondo del gráfico.

Un último detalle que merece ser mencionado es que en el caso de utilizar la clase de colocación de la imagen de fondo en un EA, debe ser lo primero que se declare en la rutina INIT, para evitar que la imagen de fondo se superponga con otros objetos gráficos creados por el EA.

Disfrute del resultado final con moderación, porque ahora estará aún más inmerso en los gráficos...



Traducción del portugués realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/pt/articles/10215

Archivos adjuntos |
EA_With_Wallpaper.zip (7778.85 KB)
Rodrigo De Souza Bonatto
Rodrigo De Souza Bonatto | 2 feb. 2022 en 21:59
Muy bueno
[Eliminado] | 27 abr. 2022 en 20:45
La utilidad funciona perfectamente, pero algunos indicadores que utilizan OnChartEvent dejan de funcionar y no se puede hacer clic una vez que aplico este EA para el fondo
Daniel Jose
Daniel Jose | 2 may. 2022 en 14:32
Arpit Tailang # : O utilitário funciona perfeitamente, além de alguns sinalizadores que OnChartEvent usa para funcionar e se tornam inclicáveis ​​quando aplico este EA em segundo plano

De fato, aconteceu algo envolvendo as atualizações do MT5, que transformaram o Wallpaper em algo diferente do esperado e mostrado no artigo, já que o correto que ele seja exibido no FUNDO GRÁFICO, pois na classe C_WallPaper você encontrará a seguinte linha:

ObjectSetInteger (Terminal.Get_ID(), szName, OBJPROP_BACK , true ); 

Isso informa que o objeto terá que ficar  no fundo, mais estranhamente, ele está vindo para a frente, por conta disto o OBJETO que recebe ou BitMap começa a receber os cliques, uma solução seria aumentar o Status de todos os objetos, ou tentar baixar o status do Bitmap, no caso esta segunda seria mais simples, isto seria conseguido alterando valor da propriedade OBJPROP_ZORDER no objeto que recebe o Wallpaper,  Tentei as duas soluções, mas não consegui estabilizar a coisa toda de  forma a corrigir o problema, portanto, e INFELIZMENTE, o papel de parede deve ser descartado por hora ... Se você prestar atenção, irá que a imagem do bitmap estará sendo plotada no corpo dos candles, indicando que o objeto bitmap esta em primeiro plano, novamente isto não é o comportamento esperado por conta da linha de código acima, por conta disto é que ele passa a receber todo e qualquer evento de clique ... 🙁

felipe ramos
felipe ramos | 2 may. 2022 en 20:40
La posibilidad de utilizar la función que se presenta en este código
//+------------------------------------------------------------------+
//|                                                   SelectFile.mqh |
//+------------------------------------------------------------------+
#include <Controls\Dialog.mqh>
#include <Controls\Button.mqh>
#include <Controls\Edit.mqh>
#include <Controls\ListView.mqh>
#include <Controls\CheckGroup.mqh>
//+------------------------------------------------------------------+
//| defines                                                          |
//+------------------------------------------------------------------+
#define  INDENT_LEFT             (4)             // indent from left
#define  INDENT_TOP              (4)             // indent from top
#define  INDENT_RIGHT            (4)             // indent from right
#define  BUTTON_WIDTH            (60)     // size by X coordinate
#define  BUTTON_HEIGHT   (20)       // size by Y coordinate
#define  EDIT_HEIGHT             (20)       // size by Y coordinate
#define  COMMON_WIDTH            (90)       // size by X coordinate
//+------------------------------------------------------------------+
//| Class CSelectFile                                                |
//+------------------------------------------------------------------+
class CSelectFile : public CDialog
  {
   CEdit             m_filename;
   CButton           m_button_ok;
   CButton           m_button_cancel;
   CListView         m_filelist;
   CCheckGroup       m_common;
   string            m_instance_id,
                     m_files[],
                     m_folders[],
                     m_prevfolder,
                     m_cfolder,
                     m_fullname;
   int               m_numberfiles,
                     m_numberfolders,
                     m_totalfiles,
                     m_fileflag,
                     m_pressbutton;
protected:
   CChart            m_chart;
public:
                     CSelectFile(void);
                    ~CSelectFile(void);
   virtual bool      Create(const long chart,const string name,const int x1,const int y1,const int x2,const int y2);
   int               ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam);
   string            Filename(){ m_pressbutton=0; return(m_fullname);}
   int               FileFlag(){ return(m_fileflag);}
protected:
   void              OnClickButtonOK(void);
   void              OnClickButtonCancel(void);
   virtual void      OnClickButtonClose(void);
   void              OnChangeList(void);
   void              OnCheckCommon(void);
   void              SetFolder(string m_fol="");
  };
//+------------------------------------------------------------------+
CSelectFile::CSelectFile(void)
  {
   m_instance_id=IntegerToString(rand(),5,'0');
   m_fileflag=0;
   m_pressbutton=0;
   m_fullname="";
   m_cfolder="";
   m_prevfolder="";
   m_numberfiles=0;
   m_numberfolders=0;
  }
//+------------------------------------------------------------------+
CSelectFile::~CSelectFile(void)
  {
   ArrayFree(m_folders);
   ArrayFree(m_files);
   m_chart.Detach();
   CDialog::Destroy();
  }
//+------------------------------------------------------------------+
bool CSelectFile::Create(const long chart,const string name,const int x1,const int y1,const int x2,const int y2)
  {
   if(x2-x1<280 || y2-y1<200) return(false);
   m_chart_id=chart;
   m_name=name;
   m_subwin=0;
//--- initialize chart object
   m_chart.Attach(chart);
//--- specify object and mouse events
   if(!m_chart.EventObjectCreate() || !m_chart.EventObjectDelete() || !m_chart.EventMouseMove())
     {
      Print("CSelectFile: object events specify error");
      m_chart.Detach();
      return(false);
     }
//--- call method of the parent class
   if(!CDialog::Create(m_chart.ChartId(),m_instance_id,m_subwin,x1,y1,x2,y2))
     {
      Print("CSelectFile: expert dialog create error");
      m_chart.Detach();
      return(false);
     }
   Caption(name);
//--- create dependent controls
//--- create list of files
   int _x1=INDENT_LEFT;
   int _y1=INDENT_TOP;
   int _x2=ClientAreaWidth()-INDENT_RIGHT;
   int _y2=_y1+(ClientAreaHeight()-INDENT_TOP*5-EDIT_HEIGHT*2);
   if(!m_filelist.Create(m_chart_id,m_name+"FileList",m_subwin,_x1,_y1,_x2,_y2)) return(false);
   if(!Add(m_filelist)) return(false);
   m_prevfolder="";
   m_cfolder="";
   SetFolder(m_cfolder);
//--- create field of filename
   _x1=INDENT_LEFT;
   _y1=INDENT_TOP+_y2;
   _x2=ClientAreaWidth()-INDENT_RIGHT;
   _y2=_y1+EDIT_HEIGHT+INDENT_TOP;
   if(!m_filename.Create(m_chart_id,m_name+"Filename",m_subwin,_x1,_y1,_x2,_y2)) return(false);
   if(!Add(m_filename)) return(false);
//--- create common check
   _x1=INDENT_LEFT;
   _y1=INDENT_TOP+_y2;
   _x2=_x1+COMMON_WIDTH;
   _y2=_y1+EDIT_HEIGHT;
   if(!m_common.Create(m_chart_id,m_name+"Common",m_subwin,_x1,_y1,_x2,_y2)) return(false);
   if(!Add(m_common)) return(false);
   if(!m_common.AddItem("Common",1)) return(false);
//--- create button Cancel
   _x1=ClientAreaWidth()-INDENT_RIGHT-BUTTON_WIDTH;
   _x2=_x1+BUTTON_WIDTH;
   _y2=_y1+BUTTON_HEIGHT;
   if(!m_button_cancel.Create(m_chart_id,m_name+"Cancel",m_subwin,_x1,_y1,_x2,_y2)) return(false);
   if(!m_button_cancel.Text("Cancel")) return(false);
   if(!Add(m_button_cancel)) return(false);
//--- create button OK
   _x1=_x1-INDENT_RIGHT-BUTTON_WIDTH;
   _x2=_x1+BUTTON_WIDTH;
   if(!m_button_ok.Create(m_chart_id,m_name+"OK",m_subwin,_x1,_y1,_x2,_y2)) return(false);
   if(!m_button_ok.Text("OK")) return(false);
   if(!Add(m_button_ok)) return(false);
//----
   m_pressbutton=0;
   m_fullname="";
   m_chart.Redraw();
//----
   if(Id(m_subwin*CONTROLS_MAXIMUM_ID)>CONTROLS_MAXIMUM_ID)
     {
      Print("CSelectFile: too many objects");
      return(false);
     }
   return(true);
  }
//+------------------------------------------------------------------+
//| Event handler                                                    |
//+------------------------------------------------------------------+
int CSelectFile::ChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
   if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_button_ok.Id()) OnClickButtonOK();
   else if(id==(ON_CLICK+CHARTEVENT_CUSTOM) && lparam==m_button_cancel.Id()) OnClickButtonCancel();
   else if(id==(ON_CHANGE+CHARTEVENT_CUSTOM) && lparam==m_filelist.Id()) OnChangeList();
   else if(id==(ON_CHANGE+CHARTEVENT_CUSTOM) && lparam==m_common.Id()) OnCheckCommon();
   else if(!CDialog::OnEvent(id,lparam,dparam,sparam)) return(0);
   m_chart.Redraw();
   return(m_pressbutton);
  }
//+------------------------------------------------------------------+
//|                                                                  |
//+------------------------------------------------------------------+
void CSelectFile::OnClickButtonOK(void)
  {
   m_fullname=m_filename.Text();
   StringTrimLeft(m_fullname);
   StringTrimRight(m_fullname);
   if(StringLen(m_fullname)>0)
     {
      m_fullname=m_cfolder+m_fullname;
      m_pressbutton=1;
     }
  }
//+------------------------------------------------------------------+
void CSelectFile::OnClickButtonCancel(void)
  {
   m_pressbutton=-1;
   m_fullname="";
  }
//+------------------------------------------------------------------+
void CSelectFile::OnClickButtonClose(void)
  {
   OnClickButtonCancel();
  }
//+------------------------------------------------------------------+
void CSelectFile::OnChangeList(void)
  {
   int i=(int)m_filelist.Value();
   if(i<0) return;
   else if(i==0)
     {
      string s;
      if(m_cfolder==m_prevfolder || s==m_prevfolder)
        {
         m_cfolder="";
         m_prevfolder=m_cfolder;
         SetFolder(m_cfolder);
        }
      else
        {
         s="\\"+m_prevfolder;
         StringReplace(m_cfolder,s,"\\");
         m_prevfolder=m_cfolder;
         if(m_cfolder=="\\") m_cfolder="";
         SetFolder(m_cfolder+"\\");
        }
      m_filename.Text("");
     }
   else if(i<m_numberfolders)
     {
      m_prevfolder=m_folders[i];
      m_cfolder+=m_prevfolder;
      SetFolder(m_cfolder+"\\");
      m_filename.Text("");
     }
   else m_filename.Text(m_filelist.Select());
  }
//+------------------------------------------------------------------+
void CSelectFile::OnCheckCommon(void)
  {
   m_fileflag=m_common.Value()>0?FILE_COMMON:0;
   if(m_fileflag==0)
     {
      m_prevfolder="";
      m_cfolder="";
      SetFolder(m_cfolder);
      m_filename.Text("");
     }
   else
     {
      m_prevfolder="";
      m_cfolder="";
      SetFolder(m_cfolder);
      m_filename.Text("");
     }
  }
//+------------------------------------------------------------------+
void CSelectFile::SetFolder(string fol="")
  {
   string fl,ff,fld=fol;
   StringReplace(fld,"\\\\","\\");
   int i;
   m_filelist.Select(0);
   m_filelist.ItemsClear();
   ArrayResize(m_folders,1);
   ArrayResize(m_files,1);
   if(fld=="Files\\") fl=""; else fl=fld;
   //---folders
   long  hfind=FileFindFirst(fl+"*",ff,m_fileflag);
   if(hfind==INVALID_HANDLE)
     {//empty folder
      m_numberfiles=0;
      m_numberfolders=1;
      m_folders[0]="Files\\"+fld;
      m_filelist.ItemAdd(m_folders[0]);
      m_totalfiles=0;
     }
   else
     {
      m_numberfiles=0;
      m_numberfolders=0;
      do
        {
         if(StringFind(ff,"\\")>1) m_numberfolders++;
         m_numberfiles++;
        }
      while(FileFindNext(hfind,ff));
      FileFindClose(hfind);
      ArrayResize(m_folders,m_numberfolders+1);
      hfind=FileFindFirst(fl+"*",ff,m_fileflag);
      if(hfind==INVALID_HANDLE) return;
      m_numberfolders=1;
      do
        {
         if(StringFind(ff,"\\")>1){ m_folders[m_numberfolders]=ff; m_numberfolders++;}
        }
      while(FileFindNext(hfind,ff));
      FileFindClose(hfind);
      if(fld=="")
        {
         m_folders[0]="Files\\";
         ff="";
        }
      else
        {
         m_folders[0]=fld;
         ff="Files\\";
        }
      m_filelist.ItemAdd(ff+m_folders[0]);
      int nn=m_numberfolders;
      for(i=1; i<nn; i++) m_filelist.ItemAdd(m_folders[i]);
      //---files
      hfind=FileFindFirst(fl+"*.*",ff,m_fileflag);
      m_numberfiles=0;
      do
        {
         if(StringFind(ff,"\\")<0) m_numberfiles++;
        }
      while(FileFindNext(hfind,ff));
      FileFindClose(hfind);
      if(m_numberfiles>0)
        {
         ArrayResize(m_files,m_numberfiles);
         m_numberfiles=0;
         hfind=FileFindFirst(fl+"*.*",ff,m_fileflag);
         if(hfind!=INVALID_HANDLE)
           {
            do
              {
               if(StringFind(ff,"\\")<0) { m_files[m_numberfiles]=ff; m_numberfiles++;}
              }
            while(FileFindNext(hfind,ff));
            FileFindClose(hfind);
           }
         for(i=0; i<m_numberfiles; i++) m_filelist.ItemAdd(m_files[i]);
        }
      m_totalfiles=m_numberfiles+m_numberfolders+1;
     }
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                              Test_SelectFile.mq5 |
//+------------------------------------------------------------------+
#include <class_SelectFile.mqh>
//+------------------------------------------------------------------+
CSelectFile *SelectFile=NULL;
//+------------------------------------------------------------------+
int OnInit()
  {
   SelectFile=new CSelectFile();
   if(!SelectFile.Create(0,"Select a file",20,20,350,300)) return(-1);
   return(0);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(SelectFile!=NULL) delete SelectFile;
  }
//+------------------------------------------------------------------+
void OnChartEvent(const int id,         // event ID  
                  const long& lparam,   // event parameter of the long type
                  const double& dparam, // event parameter of the double type
                  const string& sparam) // event parameter of the string type
  {
   if(SelectFile!=NULL)
     {
      int key=SelectFile.ChartEvent(id,lparam,dparam,sparam);
      if(key>0)
        {//press button OK
         string file=SelectFile.Filename();
         int flag=SelectFile.FileFlag();
         delete SelectFile;
         SelectFile=NULL;
         Print("The selected file - ",flag==FILE_COMMON?"[Common]":"",file);
         int handle=FileOpen(file,flag);
         if(handle>0)
           {
            Print(file," - open");
            FileClose(handle);
           }
         else Print(file," - failed to open");
        }
      else if(key<0)
        {//press button Cancel
         delete SelectFile;
         SelectFile=NULL;
         Print("No file selected");
        }
     }
   if(SelectFile==NULL)
     {
      Print("The program is completed");
      ExpertRemove();
     }
  }
//+------------------------------------------------------------------+
tipo mudar a imem en el gráfico al hacer clic en el archivo. queria saber si es posible, si no lo es y por complejo eu evito quebra mis neurônios kkkkkk
Guilherme Mendonca
Guilherme Mendonca | 3 may. 2022 en 20:05
Daniel Jose #:

De hecho, algo pasó con las actualizaciones de MT5, que transforman el Wallpaper en algo diferente a lo esperado y mostrado en el artículo, ya que lo correcto es que se muestre en el GRAPHIC BACKGROUND, porque en la clase C_WallPaper se encuentra la siguiente línea:

Esto informa que el objeto deberá permanecer en segundo plano, más extrañamente, está pasando al frente, debido a esto el OBJETO que recibe o BitMap comienza a recibir los clics, una solución sería aumentar el Estado de todos los objetos, o tratar de bajar el estado del Bitmap, en el caso este segundo sería más sencillo, esto se lograría cambiando el valor de la propiedad OBJPROP_ZORDER en el objeto que recibe el Wallpaper, probé ambas soluciones, pero no pude estabilizar el conjunto de manera de corregir el problema, por lo tanto, e INFELIZMENTE, el wallpaper debe ser descartado por ahora ... Si prestas atención verás que la imagen del mapa de bits está siendo trazada en el cuerpo de las velas, indicando que el objeto mapa de bits está en primer plano, de nuevo este no es el comportamiento esperado debido a la línea de código anterior, debido a esto sucede que recibe todos y cada uno de los eventos de clic... 🙁


Daniel,

¿Es posible si se pone un temporizador y se ejecuta una función para desactivar y volver a activar el fondo de pantalla o algo que "reafirme" la función poniendo el fondo de pantalla en segundo plano (de vuelta)?
Un panel GUI que utilizo, tuve que optar por esta solución. Algunos elementos gráficos deben ser eliminados y creados de nuevo para estar en el fondo o en el frente, dependiendo del tipo de objeto.

Indicadores multiples en un gráfico (Parte 03):  Desarrollo de definiciones para los usuarios Indicadores multiples en un gráfico (Parte 03): Desarrollo de definiciones para los usuarios
Es la primera vez que se actualiza la funcionalidad del sistema de indicadores. En el artículo anterior Múltiples indicadores en un gráfico expliqué el código base para poder utilizar más de un indicador dentro de una subventana, pero lo que se presentó era sólo la base inicial de un sistema mucho mayor.
Gráficos en la biblioteca DoEasy (Parte 96): Trabajando con los eventos del ratón y los gráficos en los objetos de formulario Gráficos en la biblioteca DoEasy (Parte 96): Trabajando con los eventos del ratón y los gráficos en los objetos de formulario
En este artículo, comenzaremos a desarrollar las funciones necesarias para trabajar con los eventos del ratón en los objetos de formulario y añadiremos nuevas propiedades y la monitorización de las mismas al objeto de símbolo. Además, hoy finalizaremos la clase de objeto símbolo, ya que, desde el momento en que la escribimos, los símbolos gráficos han adquirido nuevas propiedades que debemos considerar, y cuyos cambios tenemos que monitorear.
Plantilla para proyectar el MVC y posibilidades de uso (Parte 2): Esquema de interacción entre los tres componentes Plantilla para proyectar el MVC y posibilidades de uso (Parte 2): Esquema de interacción entre los tres componentes
Este artículo continúa y completa el tema planteado en el último artículo: la plantilla MVC en los programas MQL. En este artículo, veremos un posible esquema de interacción entre estos tres componentes.
Múltiples indicadores en un gráfico (Parte 02): primeros experimentos Múltiples indicadores en un gráfico (Parte 02): primeros experimentos
En el artículo anterior, múltiples indicadores en un gráfico, presenté los conceptos y fundamentos para que podamos utilizar múltiples indicadores en un gráfico. Aquí presentaré y desglosaré el código fuente.