Guardar imágenes en un archivo: ResourceSave

La API de MQL5 permite escribir un recurso en un archivo BMP mediante la función ResourceSave. Actualmente, el marco sólo admite recursos de imagen.

bool ResourceSave(const string resource, const string filename)

Los parámetros resource y filename especifican el nombre del recurso y del archivo, respectivamente. El nombre del recurso debe empezar por «::». El nombre del archivo puede contener una ruta relativa a la carpeta MQL5/Files. Si es necesario, la función creará todos los subdirectorios intermedios. Si el archivo especificado existe, se sobrescribirá.

La función devuelve true en caso de éxito.

Para comprobar el funcionamiento de esta función conviene crear una imagen original. Tenemos exactamente la imagen adecuada para ello.

Como parte del estudio de la POO, en el capítulo Clases e interfaces iniciamos una serie de ejemplos sobre formas gráficas: desde la primera versión Shapes1.mq5 en la sección sobre Definición de clases a la última versión Shapes6.mq5 en la sección sobre Tipos anidados. Entonces, dibujar no estaba a nuestro alcance, y no fue sino en el capítulo sobre objetos gráficos que pudimos implementar la visualización en el script ObjectShapesDraw.mq5. Ahora, tras estudiar los recursos gráficos, ha llegado el momento de otra «mejora».

En la nueva versión del script ResourceShapesDraw.mq5 dibujaremos las formas. Para facilitar el análisis de los cambios con respecto a la versión anterior, mantendremos el mismo conjunto de formas: rectángulo, cuadrado, óvalo, círculo y triángulo. Esto se hace para dar un ejemplo, y no porque algo nos limite a la hora de dibujar; al contrario: existe un potencial para ampliar el conjunto de formas, efectos visuales y etiquetado. Veremos las características en algunos ejemplos, empezando por el actual. No obstante, tenga en cuenta que no es posible demostrar toda la gama de aplicaciones en el ámbito de este libro.

Una vez generadas y dibujadas las formas, guardamos el recurso resultante en un archivo.

La base de la jerarquía de clases de forma es la clase Shape que tenía un método draw.

class Shape
{
public:
   ...
   virtual void draw() = 0;
   ...
}

En las clases derivadas, se implementó sobre la base de objetos gráficos, con llamadas a ObjectCreate y la posterior configuración de los objetos mediante las funciones de ObjectSet. El lienzo compartido de tal dibujo era el propio gráfico.

Ahora tenemos que pintar píxeles en algún recurso compartido de acuerdo con la forma en concreto. Es deseable asignar un recurso común y métodos para modificar píxeles en él en una clase separada o, mejor, una interfaz.

Una entidad abstracta nos permitirá no establecer vínculos con el método de creación y configuración del recurso. En concreto, nuestra próxima implementación colocará el recurso en un objeto OBJ_BITMAP_LABEL (como ya hemos hecho en este capítulo), y para algunos, puede ser suficiente para generar imágenes en la memoria y guardar en el disco sin trazado (ya que a muchos operadores les gusta capturar periódicamente gráficos de los estados).

Llamemos a la interfaz Drawing.

interface Drawing
{
   void point(const float x1const float y1const uint pixel);
   void line(const int x1const int y1const int x2const int y2const color clr);
   void rect(const int x1const int y1const int x2const int y2const color clr);
};

He aquí sólo tres de los métodos más básicos para dibujar, que son suficientes para este caso.

El método point es público (lo que permite poner un punto aparte), pero en cierto sentido, es de bajo nivel, ya que todos los demás se implementarán a través de él. Por ello, las coordenadas que contiene son reales, y el contenido del píxel es un valor ya preparado del tipo uint. Esto permitirá, si es necesario, aplicar diversos algoritmos de suavizado para que las formas no se vean escalonadas debido al pixelado. Aquí no abordaremos esta cuestión.

Teniendo en cuenta una interfaz, el método Shape::draw se convierte en el siguiente:

virtual void draw(Drawing *drawing) = 0;

A continuación, en la clase Rectangle, es muy fácil delegar el dibujo del rectángulo a una nueva interfaz.

class Rectangle : public Shape
{
protected:
   int dxdy// size (width, height)
   ...
public:
   void draw(Drawing *drawingoverride
   {
 // x, y - anchor point (center) in Shape
      drawing.rect(x — dx / 2y — dy / 2x + dx / 2y + dy / 2backgroundColor);
   }
};

Hay que esforzarse más para dibujar una elipse.

class Ellipse : public Shape
{
protected:
   int dxdy// large and small radii
   ...
public:
   void draw(Drawing *drawingoverride
   {
      // (x, y) - center
      const int hh = dy * dy;
      const int ww = dx * dx;
      const int hhww = hh * ww;
      int x0 = dx;
      int step = 0;
      
      // main horizontal diameter
      drawing.line(x - dxyx + dxybackgroundColor);
      
      // horizontal lines in the upper and lower half, symmetrically decreasing in length
      for(int j = 1j <= dyj++)
      {
         for(int x1 = x0 - (step - 1); x1 > 0; --x1)
         {
            if(x1 * x1 * hh + j * j * ww <= hhww)
            {
               step = x0 - x1;
               break;
            }
         }
         x0 -= step;
         drawing.line(x - x0y - jx + x0y - jbackgroundColor);
         drawing.line(x - x0y + jx + x0y + jbackgroundColor);
      }
   }
};

Por último, para el triángulo, el renderizado se implementa como sigue:

class Trianglepublic Shape
{
protected:
   int dx;  // one size, because triangles are equilateral 
   ...
public:
   virtual void draw(Drawing *drawingoverride
   {
      // (x, y) - center
      // R = a * sqrt(3) / 3
      // p0: x, y + R
      // p1: x - R * cos(30), y - R * sin(30)
      // p2: x + R * cos(30), y - R * sin(30)
      // Pythagorean height: dx * dx = dx * dx / 4 + h * h
      // sqrt(dx * dx * 3/4) = h
      const double R = dx * sqrt(3) / 3;
      const double H = sqrt(dx * dx * 3 / 4);
      const double angle = H / (dx / 2);
      
      // main vertical line (triangle height)
      const int base = y + (int)(R - H);
      drawing.line(xy + (int)RxbasebackgroundColor);
      
      // smaller vertical lines left and right, symmetrical
      for(int j = 1j <= dx / 2; ++j)
      {
         drawing.line(x - jy + (int)(R - angle * j), x - jbasebackgroundColor);
         drawing.line(x + jy + (int)(R - angle * j), x + jbasebackgroundColor);
      }
   }
};

Pasemos ahora a la clase MyDrawing, derivada de la interfaz Drawing. Se trata de MyDrawing, que debe, guiado por llamadas a métodos de interfaz en formas, garantizar que un determinado recurso se muestre en un mapa de bits. Por lo tanto, la clase describe variables para los nombres del objeto gráfico (object) y del recurso (sheet), así como el array data de tipo uint para almacenar la imagen. Además, hemos movido el array shapes de formas, declarado anteriormente en el manejador OnStart. Dado que MyDrawing es responsable de dibujar todas las formas, es mejor gestionar su conjunto aquí.

class MyDrawingpublic Drawing
{
   const string object;     // object with bitmap
   const string sheet;      // resource
   uint data[];             // pixels
   int widthheight;       // dimensions
   AutoPtr<Shapeshapes[]; // figures/shapes
   const uint bg;           // background color
   ...

En el constructor creamos un objeto gráfico para el tamaño de todo el gráfico y asignamos memoria para el array data. El lienzo se rellena con ceros (lo que significa «transparencia negra») o con cualquier valor que se pase en el parámetro background, tras lo cual se crea un recurso basado en él. De manera predeterminada, el nombre del recurso empieza por la letra «D» e incluye el ID del gráfico actual, pero puede especificar otra cosa.

public:
   MyDrawing(const uint background = 0const string s = NULL) :
      object((s == NULL ? "Drawing" : s)),
      sheet("::" + (s == NULL ? "D" + (string)ChartID() : s)), bg(background)
   {
      width = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
      height = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
      ArrayResize(datawidth * height);
      ArrayInitialize(databackground);
   
      ResourceCreate(sheetdatawidthheight00widthCOLOR_FORMAT_ARGB_NORMALIZE);
      
      ObjectCreate(0objectOBJ_BITMAP_LABEL000);
      ObjectSetInteger(0objectOBJPROP_XDISTANCE0);
      ObjectSetInteger(0objectOBJPROP_YDISTANCE0);
      ObjectSetInteger(0objectOBJPROP_XSIZEwidth);
      ObjectSetInteger(0objectOBJPROP_YSIZEheight);
      ObjectSetString(0objectOBJPROP_BMPFILEsheet);
   }

El código de llamada puede averiguar el nombre del recurso utilizando el método resource.

   string resource() const
   {
      return sheet;
   }

El recurso y el objeto se eliminan en el destructor.

   ~MyDrawing()
   {
      ResourceFree(sheet);
      ObjectDelete(0object);
   }

El método push rellena el array de formas.

   Shape *push(Shape *shape)
   {
      shapes[EXPAND(shapes)] = shape;
      return shape;
   }

El método draw dibuja las formas. Simplemente llama al método draw de cada forma en el bucle y luego actualiza el recurso y el gráfico.

   void draw()
   {
      for(int i = 0i < ArraySize(shapes); ++i)
      {
         shapes[i][].draw(&this);
      }
      ResourceCreate(sheetdatawidthheight00widthCOLOR_FORMAT_ARGB_NORMALIZE);
      ChartRedraw();
   }

A continuación se muestran los métodos más importantes que son los métodos de la interfaz Drawing y que realmente implementan el dibujo.

Empecemos por el método point, que por ahora presentamos de forma simplificada (más adelante nos ocuparemos de las mejoras).

   virtual void point(const float x1const float y1const uint pixeloverride
   {
      const int x_main = (int)MathRound(x1);
      const int y_main = (int)MathRound(y1);
      const int index = y_main * width + x_main;
      if(index >= 0 && index < ArraySize(data))
      {
         data[index] = pixel;
      }
   }

Basándose en point, es fácil implementar el dibujo de líneas. Cuando las coordenadas de los puntos inicial y final coinciden en una de las dimensiones, utilizamos el método rect para dibujar, ya que una línea recta es un caso degenerado de un rectángulo de espesor unitario.

   virtual void line(const int x1const int y1const int x2const int y2const color clroverride
   {
      if(x1 == x2rect(x1y1x1y2clr);
      else if(y1 == y2rect(x1y1x2y1clr);
      else
      {
         const uint pixel = ColorToARGB(clr);
         double angle = 1.0 * (y2 - y1) / (x2 - x1);
         if(fabs(angle) < 1// step along the axis with the largest distance, x
         {
            const int sign = x2 > x1 ? +1 : -1;
            for(int i = 0i <= fabs(x2 - x1); ++i)
            {
               const float p = (float)(y1 + sign * i * angle);
               point(x1 + sign * ippixel);
            }
         }
         else // or y-step
         {
            const int sign = y2 > y1 ? +1 : -1;
            for(int i = 0i <= fabs(y2 - y1); ++i)
            {
               const float p = (float)(x1 + sign * i / angle);
               point(py1 + sign * ipixel);
            }
         }
      }
   }

Y he aquí el método rect:

   virtual void rect(const int x1const int y1const int x2const int y2const color clroverride
   {
      const uint pixel = ColorToARGB(clr);
      for(int i = fmin(x1x2); i <= fmax(x1x2); ++i)
      {
         for(int j = fmin(y1y2); j <= fmax(y1y2); ++j)
         {
            point(ijpixel);
         }
      }
   }

Ahora necesitamos modificar el manejador OnStart, y el script estará listo.

En primer lugar, configuramos el gráfico (ocultamos todos los elementos). En teoría, esto no es necesario: se deja para que coincida con el script prototipo.

void OnStart()
{
   ChartSetInteger(0CHART_SHOWfalse);
   ...

A continuación, describimos el objeto de la clase MyDrawing, generamos un número predefinido de formas aleatorias (aquí todo permanece inalterado, incluido el generador addRandomShape y la macro FIGURES igual a 21), las dibujamos en el recurso y las mostramos en el objeto del gráfico.

   MyDrawing raster;
   
   for(int i = 0i < FIGURES; ++i)
   {
      raster.push(addRandomShape());
   }
   
   raster.draw(); // display the initial state
   ...

En el ejemplo ObjectShapesDraw.mq5, iniciamos un bucle sin fin en el que movemos las piezas aleatoriamente. Repitamos este truco aquí. Aquí tendremos que añadir la clase MyDrawing ya que el array de formas se almacena dentro de ella. Escribamos un método sencillo shake.

class MyDrawingpublic Drawing
{
public:
   ...
   void shake()
   {
      ArrayInitialize(databg);
      for(int i = 0i < ArraySize(shapes); ++i)
      {
         shapes[i][].move(random(20) - 10random(20) - 10);
      }
   }
   ...
};

A continuación, en OnStart, podemos utilizar el nuevo método en un bucle hasta que el usuario detenga la animación.

void OnStart()
{
   ...
   while(!IsStopped())
   {
      Sleep(250);
      raster.shake();
      raster.draw();
   }
   ...
}

En este punto, prácticamente se repite la funcionalidad del ejemplo anterior. Pero necesitamos añadir el guardado de imágenes en un archivo, así que vamos a añadir un parámetro de entrada SaveImage.

input bool SaveImage = false;

Cuando se establece en true, compruebe el rendimiento de la función ResourceSave.

void OnStart()
{
   ...
   if(SaveImage)
   {
      const string filename = "temp.bmp";
      if(ResourceSave(raster.resource(), filename))
      {
         Print("Bitmap image saved: "filename);
      }
      else
      {
         Print("Can't save image "filename", "E2S(_LastError));
      }
   }
}

Además, ya que estamos hablando de variables de entrada, deje que el usuario seleccione un fondo y pase el valor resultante al constructor MyDrawing.

input color BackgroundColor = clrNONE;
void OnStart()
{
   ...
   MyDrawing raster(BackgroundColor != clrNONE ? ColorToARGB(BackgroundColor) : 0);
   ...
}

Así pues, todo está listo para la primera prueba.

Si ejecuta el script ResourceShapesDraw.mq5, el gráfico formará una imagen como la siguiente:

Mapa de bits de un recurso con un conjunto de formas aleatorias

Mapa de bits de un recurso con un conjunto de formas aleatorias

Al comparar esta imagen con lo que vimos en el ejemplo ObjectShapesDraw.mq5 resulta que nuestra nueva forma de renderizar es algo diferente a cómo el terminal muestra los objetos. Aunque las formas y los colores son correctos, los lugares donde se superponen las formas están indicados de forma diferente.

Nuestro script pinta las formas con el color especificado, superponiéndolas unas sobre otras en el orden en que aparecen en el array. Las formas posteriores se superponen a las anteriores. El terminal, por su parte, aplica algún tipo de mezcla de colores (inversión) en los lugares de solapamiento.

Ambos métodos tienen derecho a existir, aquí no hay errores. Sin embargo, ¿es posible conseguir un efecto similar al dibujar?

Tenemos control total sobre el proceso de dibujo, por lo que se le puede aplicar cualquier efecto, no sólo el del terminal.

Además de la forma original y sencilla de dibujar, implementemos algunos modos más. Todos ellos se resumen en la enumeración COLOR_EFFECT.

enum COLOR_EFFECT
{
   PLAIN,         // simple drawing with overlap (default)
   COMPLEMENT,    // draw with a complementary color (like in the terminal) 
   BLENDING_XOR,  // mixing colors with XOR '^'
   DIMMING_SUM,   // "darken" colors with '+'
   LIGHTEN_OR,    // "lighten" colors with '|'
};

Añadamos una variable de entrada para seleccionar el modo.

input COLOR_EFFECT ColorEffect = PLAIN;

Apoyemos los modos en la clase MyDrawing. En primer lugar, describamos el campo y el método correspondientes.

class MyDrawingpublic Drawing
{
   ...
   COLOR_EFFECT xormode;
   ...
public:
   void setColorEffect(const COLOR_EFFECT x)
   {
      xormode = x;
   }
   ...

A continuación, mejoramos el método point.

   virtual void point(const float x1const float y1const uint pixeloverride
   {
      ...
      if(index >= 0 && index < ArraySize(data))
      {
         switch(xormode)
         {
         case COMPLEMENT:
            data[index] = (pixel ^ (1 - data[index])); // blending with complementary color
            break;
         case BLENDING_XOR:
            data[index] = (pixel & 0xFF000000) | (pixel ^ data[index]); // direct mixing (XOR)
            break;
         case DIMMING_SUM:
            data[index] =  (pixel + data[index]); // "darkening" (SUM)
            break;
         case LIGHTEN_OR:
            data[index] =  (pixel & 0xFF000000) | (pixel | data[index]); // "lightening" (OR)
            break;
         case PLAIN:
         default:
            data[index] = pixel;
         }
      }
   }

Puede probar a ejecutar el script en diferentes modos y comparar los resultados. No olvide la posibilidad de personalizar el fondo. He aquí un ejemplo de cómo queda el aclaramiento.

Imagen de figuras con mezcla de colores aclarados

Imagen de formas con mezcla de colores aclarados

Para apreciar visualmente la diferencia de efectos, puede desactivar la aleatorización de colores y el movimiento de formas. La forma estándar de superponer objetos corresponde a la constante COMPLEMENT.

Como experimento final, active la opción SaveImage. En el manejador OnStart, al generar el nombre del archivo con la imagen, usamos ahora el nombre del modo actual. Necesitamos obtener una copia de la imagen del gráfico en el archivo.

   ...
   if(SaveImage)
   {
      const string filename = EnumToString(ColorEffect) + ".bmp";
      if(ResourceSave(raster.resource(), filename))
      ...

Para construcciones gráficas más sofisticadas de nuestra interfaz, Drawing puede no ser suficiente. Por lo tanto, puede utilizar clases de dibujo ya preparadas suministradas con MetaTrader 5 o disponibles en la base de código mql5.com. En concreto, eche un vistazo al archivo MQL5/Include/Canvas/Canvas.mqh.