Leer y modificar datos de recursos: RecursoReadImage

La función ResourceReadImage permite leer los datos del recurso creado por la función ResourceCreate o incrustado en el ejecutable en tiempo de compilación según la directiva #resource. A pesar del sufijo «Image» del nombre, la función opera con cualquier array de datos, incluidos los personalizados (véase el ejemplo de Reservoir.mq5 más abajo).

bool ResourceReadImage(const string resource, uint &data[], uint &width, uint &height)

El nombre del recurso se especifica en el parámetro resource. Para acceder a sus propios recursos, basta con la forma abreviada «::nombre_del_recurso». Para leer un recurso de otro archivo compilado, necesita el nombre completo seguido de la ruta según las reglas de resolución de rutas descritas en la sección sobre recursos. En concreto, una ruta que empiece por una barra invertida significa la ruta desde la carpeta raíz del MQL5 (de esta forma se busca «\\path\\filename.ex5::nombre_del_recurso» en el archivo /MQL5/path/filename.ex5 bajo el nombre «nombre_del_recurso»), y la ruta sin este carácter inicial significa la ruta relativa a la carpeta donde se encuentra el programa ejecutado.

La información interna del recurso se escribirá en el array receptor data, y los parámetros width y height recibirán, respectivamente, la anchura y la altura, es decir, el tamaño del array (width*height) de forma indirecta. Por separado, width y height solo son relevantes si la imagen está almacenada en el recurso. El array debe ser dinámico o fijo, pero de tamaño suficiente. De lo contrario, obtendremos un error SMALL_ARRAY (5052).

Si en el futuro desea crear un recurso gráfico basado en el array data, entonces el recurso fuente deberá utilizar el formato de color COLOR_FORMAT_ARGB_NORMALIZE o COLOR_FORMAT_XRGB_NOALPHA. Si el array data contiene datos arbitrarios de la aplicación, utilice COLOR_FORMAT_XRGB_NOALPHA.

Como primer ejemplo, consideremos el script ResourceReadImage.mq5, que sirve para hacer una demostración de varios aspectos del trabajo con recursos gráficos:

  • Creación de un recurso de imagen a partir de un archivo externo
  • Lectura y modificación de los datos de esta imagen en otro recurso creado dinámicamente
  • Conservación de los recursos creados en la memoria del terminal entre lanzamientos de scripts
  • Utilización de recursos en objetos del gráfico
  • Eliminación de un objeto y sus recursos

La modificación de la imagen en este caso concreto significa la inversión de todos los colores (como lo más visual).

Todos los métodos de trabajo anteriores se realizan en tres etapas: cada etapa se realiza en una ejecución del script. El script determina la etapa actual mediante el análisis de los recursos disponibles y el objeto:

  1. En ausencia de los recursos gráficos necesarios, el script los creará (una imagen original y otra invertida).
  2. Si hay recursos pero no hay objeto gráfico, el script creará un objeto con dos imágenes del primer paso para los estados activado/desactivado (se pueden cambiar haciendo clic con el ratón).
  3. Si hay un objeto, el script borrará el objeto y los recursos.

La función principal del script comienza definiendo los nombres de los recursos y del objeto en el gráfico.

void OnStart()
{
   const static string resource = "::Images\\pseudo.bmp";
   const static string inverted = resource + "_inv";
   const static string object = "object";
   ...

Tenga en cuenta que hemos elegido un nombre para el recurso original que se parece a la ubicación del archivo bmp en la carpeta estándar Images, pero no existe tal archivo. Esto pone de relieve la naturaleza virtual de los recursos y le permite hacer sustituciones para cumplir requisitos técnicos o dificultar la ingeniería inversa de sus programas.

La siguiente llamada a ResourceReadImage se utiliza para comprobar si el recurso ya existe. En el estado inicial (en la primera ejecución), obtendremos un resultado negativo (false) y comenzaremos el primer paso: creamos el recurso original a partir del archivo «\Images\dollar.bmp» y luego lo invertimos en un nuevo recurso con el sufijo «_inv».

   uint data[], widthheight;
   // check for resource existence
   if(!PRTF(ResourceReadImage(resourcedatawidthheight)))
   {
      Print("Initial state: Creating 2 bitmaps");
      PRTF(ResourceCreate(resource"\\Images\\dollar.bmp")); // try "argb.bmp"
      ResourceCreateInverted(resourceinverted);
   }
   ...

A continuación se presenta el código fuente de la función de ayuda ResourceCreateInverted.

Si se encuentra el recurso (segunda ejecución), el script comprueba la existencia del objeto y, si es necesario, lo crea, incluyendo la configuración de propiedades con recursos de imagen en la función ShowBitmap (véase más adelante).

   else
   {
      Print("Resources (bitmaps) are detected");
      if(PRTF(ObjectFind(0object) < 0))
      {
         Print("Active state: Creating object to draw 2 bitmaps");
         ShowBitmap(objectresourceinverted);
      }
      ...

Si tanto los recursos como el objeto ya están en el gráfico, entonces estamos en la etapa final y debemos eliminar todos los recursos.

      else
      {
         Print("Cleanup state: Removing object and resources");
         PRTF(ObjectDelete(0object));
         PRTF(ResourceFree(resource));
         PRTF(ResourceFree(inverted));
      }
   }
}

La función ResourceCreateInverted utiliza la llamada ResourceReadImage para obtener un array de píxeles y luego invierte el color en ellos utilizando el operador '^' (XOR) y un operando con todos los bits singulares en los componentes de color.

bool ResourceCreateInverted(const string resourceconst string inverted)
{
   uint data[], widthheight;
   PRTF(ResourceReadImage(resourcedatawidthheight));
   for(int i = 0i < ArraySize(data); ++i)
   {
      data[i] = data[i] ^ 0x00FFFFFF;
   }
   return PRTF(ResourceCreate(inverteddatawidthheight000,
      COLOR_FORMAT_ARGB_NORMALIZE));
}

El nuevo array data se transfiere a ResourceCreate para crear la segunda imagen.

La función ShowBitmap crea un objeto gráfico de la forma habitual (en la esquina inferior derecha del gráfico) y establece sus propiedades para los estados activado y desactivado a las imágenes original e invertida, respectivamente.

void ShowBitmap(const string nameconst string resourceOnconst string resourceOff = NULL)
{
   ObjectCreate(0nameOBJ_BITMAP_LABEL000);
   
   ObjectSetString(0nameOBJPROP_BMPFILE0resourceOn);
   if(resourceOff != NULLObjectSetString(0nameOBJPROP_BMPFILE1resourceOff);
   ObjectSetInteger(0nameOBJPROP_XDISTANCE50);
   ObjectSetInteger(0nameOBJPROP_YDISTANCE50);
   ObjectSetInteger(0nameOBJPROP_CORNERCORNER_RIGHT_LOWER);
   ObjectSetInteger(0nameOBJPROP_ANCHORANCHOR_RIGHT_LOWER);
}

Como el objeto recién creado está desactivado por defecto, primero veremos la imagen invertida y podremos cambiarla por la original al hacer clic con el ratón. Pero recordemos que nuestro script realiza acciones paso a paso, y por lo tanto, antes de que la imagen aparezca en el gráfico, el script debe ejecutarse dos veces. En todas las etapas se registran el estado actual y las acciones realizadas (junto con una indicación de éxito o error).

Tras el primer lanzamiento, aparecerán las siguientes entradas en el registro:

ResourceReadImage(resource,data,width,height)=false / RESOURCE_NOT_FOUND(4016)

Initial state: Creating 2 bitmaps

ResourceCreate(resource,\Images\dollar.bmp)=true / ok

ResourceReadImage(resource,data,width,height)=true / ok

ResourceCreate(inverted,data,width,height,0,0,0,COLOR_FORMAT_XRGB_NOALPHA)=true / ok

Los registros indican que no se han encontrado los recursos y por eso el script los ha creado. Tras la segunda ejecución, el registro dirá que se han encontrado recursos (que quedaron en memoria de la ejecución anterior del script) pero que el objeto aún no está ahí, y el script lo creará basándose en los recursos.

ResourceReadImage(resource,data,width,height)=true / ok

Resources (bitmaps) are detected

ObjectFind(0,object)<0=true / OBJECT_NOT_FOUND(4202)

Active state: Creating object to draw 2 bitmaps

Veremos un objeto y una imagen en el gráfico. Se puede cambiar de estado haciendo clic con el ratón (loseventos sobre cambios de estado no se tratan aquí).

Imágenes invertida y original en un objeto de un gráfico

Imágenes invertida y original en un objeto de un gráfico

Por último, durante la tercera ejecución, el script detectará el objeto y borrará todos sus desarrollos.

ResourceReadImage(resource,data,width,height)=true / ok

Resources (bitmaps) are detected

ObjectFind(0,object)<0=false / ok

Cleanup state: Removing object and resources

ObjectDelete(0,object)=true / ok

ResourceFree(resource)=true / ok

ResourceFree(inverted)=true / ok

Después puede repetir el ciclo.

El segundo ejemplo de la sección considerará el uso de recursos para almacenar datos arbitrarios de la aplicación, es decir, una especie de portapapeles dentro del terminal (en teoría, puede haber cualquier número de tales búferes, ya que cada uno de ellos es un recurso separado con nombre). Debido a la universalidad del problema, crearemos la clase Reservoir con la funcionalidad principal (en el archivo Reservoir.mqh), y sobre su base escribiremos un script de demostración (Reservoir.mq5).

Antes de «sumergirnos» directamente en Reservoir, vamos a introducir una unión auxiliar ByteOverlay que se necesitará con frecuencia. Una unión permitirá convertir cualquier tipo simple integrado (incluidas las estructuras simples) en un array de bytes y viceversa. Por «simple» entendemos todos los tipos numéricos integrados, fecha y hora, enumeraciones, color y banderas booleanas. Sin embargo, los objetos y los arrays dinámicos ya no son sencillos y no serán compatibles con nuestro nuevo almacenamiento (debido a limitaciones técnicas de la plataforma). Las cadenas tampoco se consideran simples, pero para ellas haremos una excepción y las procesaremos de forma especial.

template<typename T>
union ByteOverlay
{
   uchar buffer[sizeof(T)];
   T value;
   
   ByteOverlay(const T &v)
   {
      value = v;
   }
   
   ByteOverlay(const uchar &bytes[], const int offset = 0)
   {
      ArrayCopy(bufferbytes0offsetsizeof(T));
   }
};

Como sabemos, los recursos se construyen sobre la base de arrays del tipo uint, por lo que describimos un array de este tipo (storage) en la clase Reservoir. Allí añadiremos todos los datos que se escribirán posteriormente en el recurso. La posición actual en el array desde la que se escriben o leen los datos se almacena en el campo offset.

class Reservoir
{
   uint storage[];
   int offset;
public:
   Reservoir(): offset(0) { }
   ...

Para colocar un array de datos de tipo arbitrario en storage, puede utilizar el método de plantilla packArray. En la primera mitad, convertimos el array pasado en un array de bytes utilizando ByteOverlay.

   template<typename T>
   int packArray(const T &data[])
   {
      const int bytesize = ArraySize(data) * sizeof(T); // TODO: check for overflow
      uchar buffer[];
      ArrayResize(bufferbytesize);
      for(int i = 0i < ArraySize(data); ++i)
      {
         ByteOverlay<Toverlay(data[i]);
         ArrayCopy(bufferoverlay.bufferi * sizeof(T));
      }
      ...

En la segunda mitad, convertimos el array de bytes en una secuencia de valores uint, que se escriben en storage con un offset. El número de elementos necesarios uint se determina teniendo en cuenta si queda un resto después de dividir el tamaño de los datos en bytes por el tamaño de uint: opcionalmente añadimos un elemento adicional.

      const int size = bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
      ArrayResize(storageoffset + size + 1);
      storage[offset] = bytesize;       // write the size of the data before the data
      for(int i = 0i < size; ++i)
      {
         ByteOverlay<uintword(bufferi * sizeof(uint));
         storage[offset + i + 1] = word.value;
      }
      
      offset = ArraySize(storage);
      
      return offset;
   }

Antes de los datos propiamente dichos, escribimos el tamaño de los datos en bytes: se trata del protocolo más pequeño posible para la comprobación de errores a la hora de recuperar datos. En el futuro sería posible también escribir los datos de typename(T) en storage.

El método devuelve la posición actual en el almacenamiento después de la escritura.

Basándonos en packArray es fácil implementar un método para guardar cadenas:

   int packString(const string text)
   {
      uchar data[];
      StringToCharArray(textdata0, -1CP_UTF8);
      return packArray(data);
   }

También existe la opción de almacenar un número independiente:

   template<typename T>
   int packNumber(const T number)
   {
      T array[1] = {number};
      return packArray(array);
   }

Un método para restaurar un array de tipo arbitrario T desde el almacenamiento de tipo uint «pierde» todas las operaciones en sentido contrario. Si se encuentran incoherencias en el tipo legible y la cantidad de datos con el almacenamiento, el método devuelve 0 (un signo de error). En modo normal, se devuelve la posición actual en el array storage (siempre es mayor que 0 si se ha leído algo con éxito).

   template<typename T>
   int unpackArray(T &output[])
   {
      if(offset >= ArraySize(storage)) return 0// out of array bounds
      const int bytesize = (int)storage[offset];
      if(bytesize % sizeof(T) != 0return 0;    // wrong data type
      if(bytesize > (ArraySize(storage) - offset) * sizeof(uint)) return 0;
      
      uchar buffer[];
      ArrayResize(bufferbytesize);
      for(int i = 0k = 0i < ArraySize(storage) - 1 - offset
         && k < bytesize; ++ik += sizeof(uint))
      {
         ByteOverlay<uintword(storage[i + 1 + offset]);
         ArrayCopy(bufferword.bufferk);
      }
      
      int n = bytesize / sizeof(T);
      n = ArrayResize(outputn);
      for(int i = 0i < n; ++i)
      {
         ByteOverlay<Toverlay(bufferi * sizeof(T));
         output[i] = overlay.value;
      }
      
      offset += 1 + bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
      
      return offset;
   }

El desempaquetado de cadenas y números se realiza llamando a unpackArray.

   int unpackString(string &output)
   {
      uchar bytes[];
      const int p = unpackArray(bytes);
      if(p == offset)
      {
         output = CharArrayToString(bytes0, -1CP_UTF8);
      }
      return p;
   }
   
   template<typename T>
   int unpackNumber(T &number)
   {
      T array[1] = {};
      const int p = unpackArray(array);
      number = array[0];
      return p;
   }

Unos sencillos métodos de ayuda permiten averiguar el tamaño del almacén y la posición actual en él, así como borrarlo.

   int size() const
   {
      return ArraySize(storage);
   }
   
   int cursor() const
   {
      return offset;
   }
   
   void clear()
   {
      ArrayFree(storage);
      offset = 0;
   }

Ahora llegamos a lo más interesante: la interacción con los recursos.

Una vez rellenado el array storage con datos de la aplicación, es fácil «moverlo» a un recurso proporcionado.

   bool submit(const string resource)
   {
      return ResourceCreate(resourcestorageArraySize(storage), 1,
         000COLOR_FORMAT_XRGB_NOALPHA);
   }

Además, podemos leer datos de un recurso en un array storage interno.

   bool acquire(const string resource)
   {
      uint widthheight;
      if(ResourceReadImage(resourcestoragewidthheight))
      {
         return true;
      }
      return false;
   }

Mostraremos en el script Reservoir.mq5 cómo utilizarlo.

En la primera mitad de OnStart describimos el nombre para el recurso de almacenamiento y el objeto de clase Reservoir, y luego «empaquetamos» secuencialmente en este objeto una cadena, la estructura MqlTick y el número double. La estructura se «envuelve» en un array de un elemento para demostrar explícitamente el método packArray. Además, luego tendremos que comparar los datos restaurados con los originales, y MQL5 no proporciona el operador '==' para estructuras. Por lo tanto, será más conveniente utilizar la función ArrayCompare.

#include <MQL5Book/Reservoir.mqh>
#include <MQL5Book/PRTF.mqh>
   
void OnStart()
{
   const string resource = "::reservoir";
   
   Reservoir res1;
   string message = "message1";     // string to write to the resource
   PRTF(res1.packString(message));
   
   MqlTick tick1[1];                // add a simple structure
   SymbolInfoTick(_Symboltick1[0]);
   PRTF(res1.packArray(tick1));
   PRTF(res1.packNumber(DBL_MAX));  // real number
   ...

Cuando todos los datos necesarios estén «empaquetados» en el objeto, escríbalos en el recurso y borre el objeto.

   res1.submit(resource);           // create a resource with storage data
   res1.clear();                    // clear the object, but not the resource

En la segunda mitad de OnStart vamos a realizar las operaciones inversas de lectura de datos del recurso.

   string reply;                    // new variable for message
   MqlTick tick2[1];                // new structure for tick
   double result;                   // new variable for number
   
   PRTF(res1.acquire(resource));    // connect the object to the given resource
   PRTF(res1.unpackString(reply));  // read line
   PRTF(res1.unpackArray(tick2));   // read simple structure
   PRTF(res1.unpackNumber(result)); // read number
   
   // output and compare data element by element
   PRTF(reply);
   PRTF(ArrayCompare(tick1tick2));
   ArrayPrint(tick2);
   PRTF(result == DBL_MAX);
   
   // make sure the storage is read completely
   PRTF(res1.size());
   PRTF(res1.cursor());
   ...

Al final, limpiamos el recurso, ya que se trata de una prueba. En tareas prácticas, lo más probable es que un programa MQL deje el recurso creado en memoria para que pueda ser leído por otros programas. En la jerarquía de nombres, los recursos se declaran anidados en el programa que los creó. Por lo tanto, para acceder desde otros programas, debe especificar el nombre del recurso junto con el nombre del programa y, opcionalmente, la ruta (si el creador del programa y el lector del programa están en carpetas diferentes). Por ejemplo, para leer un recurso recién creado desde el exterior, la ruta completa «\Scripts\MQL5Book\p7\Reservoir.ex5::reservoir» se encargará de hacerlo.

   PrintFormat("Cleaning up local storage '%s'"resource);
   ResourceFree(resource);
}

Dado que todas las llamadas a métodos principales están controladas por la macro PRTF, cuando ejecutemos el script veremos un «informe» de progreso detallado en el registro.

res1.packString(message)=4 / ok

res1.packArray(tick1)=20 / ok

res1.packNumber(DBL_MAX)=23 / ok

res1.acquire(resource)=true / ok

res1.unpackString(reply)=4 / ok

res1.unpackArray(tick2)=20 / ok

res1.unpackNumber(result)=23 / ok

reply=message1 / ok

ArrayCompare(tick1,tick2)=0 / ok

[time] [bid] [ask] [last] [volume] [time_msc] [flags] [volume_real]

[0] 2022.05.19 23:09:32 1.05867 1.05873 0.0000 0 1653001772050 6 0.00000

result==DBL_MAX=true / ok

res1.size()=23 / ok

res1.cursor()=23 / ok

Cleaning up local storage '::reservoir'

Los datos se han copiado correctamente en el recurso y luego se han restaurado desde allí.

Los programas pueden utilizar este enfoque para intercambiar datos voluminosos que no caben en mensajes personalizados (eventosCHARTEVENT_CUSTOM+). Basta con enviar en un parámetro de cadena sparam el nombre del recurso que se va a leer. Para devolver datos, cree su propio recurso con ellos y envíe un mensaje de respuesta.