Чтение и модификация данных ресурса: ResourceReadImage

Функция ResourceReadImage позволяет прочитать данные ресурса, созданного функцией ResourceCreate или встроенного в исполняемый файл во время компиляции согласно директиве #resource. Несмотря на суффикс "Image" в названии, функция работает с любыми массивами данных, в том числе пользовательского назначения (см. пример Reservoir.mq5 далее).

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

В параметре resource указывается имя ресурса. Для доступа к собственным ресурсам достаточно краткого вида "::resource_name". Для чтения ресурса из другого скомпилированного файла, необходимо имя в полном виде с указанием пути согласно правилам разрешения путей, описанных в разделе про ресурсы. В частности, путь, начинающийся с обратной косой черты, означает путь от корневой папки MQL5 (так "\\path\\filename.ex5::resource_name" ищется в файле /MQL5/path/filename.ex5 под именем "resource_name"), а путь без этого начального символа — относительно папки, в которой размещена выполняющаяся программа.

В приемный массив data будет записана внутренняя информация ресурса, а в параметры width и height — соответственно, ширина и высота, то есть размер массива (width*height) в опосредованном виде. Раздельно width и height имеют значение только в том случае, если в ресурсе хранится картинка. Массив должен быть динамическим или фиксированного, но достаточного размера — в противном случае получим ошибку SMALL_ARRAY (5052).

Если на основании массива data в дальнейшем необходимо создать графический ресурс, то в исходном ресурсе следует использовать формат цвета COLOR_FORMAT_ARGB_NORMALIZE или COLOR_FORMAT_XRGB_NOALPHA. Если массив data содержит произвольные прикладные данные, используйте COLOR_FORMAT_XRGB_NOALPHA.

В качестве первого примера рассмотрим скрипт ResourceReadImage.mq5. Он демонстрирует сразу несколько аспектов работы с графическими ресурсами:

  • создание ресурса-картинки из внешнего файла;
  • чтение и модификацию данных этой картинки в другом динамически создаваемом ресурсе;
  • сохранность созданных ресурсов в памяти терминала между запусками скрипта;
  • использование ресурсов в объектах на графике;
  • удаление объекта и ресурсов.

Под модификацией изображения в данном конкретном случае понимается инвертирование всех цветов (как наиболее наглядное).

Все перечисленные приемы работы выполняются в три этапа: каждый этап выполняется за один запуск скрипта. Текущий этап скрипт определяет за счета анализа имеющихся ресурсов и объекта:

  1. При отсутствии требуемых графических ресурсов скрипт создаст их (одно оригинальное изображение и одно инвертированное).
  2. При наличии ресурсов, но отсутствии графического объекта скрипт создаст объект с двумя изображениями с первого шага для состояний включено/выключено (их можно будет переключать по клику мыши).
  3. При наличии объекта скрипт удалит объект и ресурсы.

Главная функция скрипта начинается с определения имен для ресурсов и объекта на графике.

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

Обратите внимание, что мы выбрали имя для исходного ресурса, которое напоминает размещение bmp-файла в стандартной папке Images, однако такого файла нет. Это подчеркивает виртуальную сущность ресурсов и позволяет делать подмены согласно техническим требованиям или в целях затруднения реверсинжиниринга ваших программ.

Следующий вызов ResourceReadImage используется для того, чтобы проверить, существует ли уже ресурс. В начальном состоянии (при первом запуске) мы получим отрицательный результат (false) и начнем первый этап: создаем исходный ресурс из файла "\\Images\\dollar.bmp", а затем инвертируем его в новом ресурсе с суффиксом "_inv".

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

Исходный код вспомогательной функции ResourceCreateInverted будет представлен ниже.

Если ресурс нашелся (второй запуск), скрипт проверяет наличие объекта и при необходимости создает его, включая установку свойств с ресурсами-картинками в функции ShowBitmap (см. ниже).

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

Если же и ресурсы, и объект уже есть на графике, значит мы на заключительном этапе и должны удалить все ресурсы.

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

Функция ResourceCreateInverted использует вызов ResourceReadImage для получения массива пикселей, а затем инвертирует в них цвет с помощью оператора '^' (XOR) и операнда со всеми единичными битами в компонентах цвета.

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));
}

Новый массив data передается в ResourceCreate для создания второго изображения.

Функция ShowBitmap привычным образом создает графический объект (в правом нижнем углу графика) и устанавливает его свойства для включенного и выключенного состояния в, соответственно, оригинальное и инвертированное изображение.

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);
}

Поскольку только что созданный объект по умолчанию находится в выключенном состоянии, мы сначала увидим инвертированное изображение и сможем его переключить в оригинальное по щелчку мыши. Но напомним, что наш скрипт выполняет действия по шагам, а потому прежде чем изображение появится на графике, скрипт нужно запустить дважды. На всех этапах в журнал выводится текущее состояние и выполняемые действия (вместе с признаком успеха или ошибки).

После первого запуска в журнале появятся такие записи:

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

Они говорят, что ресурсы не были обнаружены и потому скрипт их создал. После второго запуска в журнале будет сказано об обнаружении ресурсов (которые остались в памяти с предыдущего запуска скрипта), но объекта еще нет, и скрипт создаст его на основе ресурсов.

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

Мы увидим на графике объект и изображение. Переключение состояний доступно по клику мыши (события об изменении состояния мы здесь не обрабатываем).

Инвертированное и исходное изображения в объекте на графике

Инвертированное и исходное изображения в объекте на графике

Наконец, в ходе третьего запуска скрипт обнаружит объект и удалит все свои наработки.

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

Далее можно повторять цикл.

Вторым примером раздела рассмотрим применение ресурсов для хранения произвольных прикладных данных — своего рода буфер обмена внутри терминала (таких буферов по идее может быть любое количество, т.к. каждый из них — это отдельный именованный ресурс). В силу универсальности задачи создадим класс Reservoir с основным функционалом (в файле Reservoir.mqh), а уже на его основе напишем демонстрационный скрипт (Reservoir.mq5).

Прежде чем углубляться непосредственно в Reservoir, представим вспомогательное объединение ByteOverlay, которое нам не раз потребуется. Объединение позволит конвертировать любой простой встроенный тип (включая простые структуры) в массив байтов и обратно. Напомним, что "простыми" мы называем все встроенные числовые типы, дату и время, перечисления, цвет, логические флаги. А вот объекты и динамические массивы уже не являются простыми и нашим новым хранилищем не будут поддерживаться (в силу технических ограничений платформы). Строки тоже не считаются простыми, но для них мы сделаем некое исключение — обработаем их особым образом.

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));
   }
};

Как мы знаем, ресурсы строятся на базе массивов типа uint, поэтому опишем такой массив (storage) в классе Reservoir. Туда мы будем складывать все данные, подлежащие последующей записи в ресурс. Текущая позиция в массиве, куда пишутся или откуда считываются данные, хранится в поле offset.

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

Положить массив данных произвольного типа в storage можно с помощью шаблонного метода packArray. В первой его половине мы преобразуем переданный массив в массив байтов с помощью ByteOverlay.

   template<typename T>
   int packArray(const T &data[])
   {
      const int bytesize = ArraySize(data) * sizeof(T); // TODO: проверить переполнение
      uchar buffer[];
      ArrayResize(bufferbytesize);
      for(int i = 0i < ArraySize(data); ++i)
      {
         ByteOverlay<Toverlay(data[i]);
         ArrayCopy(bufferoverlay.bufferi * sizeof(T));
      }
      ...

Во второй половине преобразуем массив байтов в последовательность значений uint, которые записываем в storage по смещению offset. Количество требуемых элементов uint определяется с учетом того, есть ли остаток от деления размера данных в байтах на размер uint: при необходимости добавляем один дополнительный элемент.

      const int size = bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
      ArrayResize(storageoffset + size + 1);
      storage[offset] = bytesize;       // размер данных пишем перед данными
      for(int i = 0i < size; ++i)
      {
         ByteOverlay<uintword(bufferi * sizeof(uint));
         storage[offset + i + 1] = word.value;
      }
      
      offset = ArraySize(storage);
      
      return offset;
   }

Перед самими данными мы пишем размер данных в байтах: это минимальный из возможных протоколов для проверки ошибок при восстановлении данных. В перспективе можно было бы записывать в storage также и typename(T) данных.

Метод возвращает текущую позицию в хранилище после записи.

На основе packArray легко реализовать метод для сохранения строк:

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

Есть также и возможность сохранить отдельное число:

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

Метод для восстановления массива произвольного типа T из хранилища типа uint "проигрывает" все операции в обратную сторону. При обнаружении нестыковок в читаемом типе и количестве данных с хранилищем метод возвращает 0 (признак ошибки). В штатном режиме возвращается текущая позиции в массиве storage (она всегда больше 0, если что-то успешно прочитано).

   template<typename T>
   int unpackArray(T &output[])
   {
      if(offset >= ArraySize(storage)) return 0// выход за границы массива
      const int bytesize = (int)storage[offset];
      if(bytesize % sizeof(T) != 0return 0;    // неподходящий тип данных
      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;
   }

Распаковка строк и чисел производится через вызов 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;
   }

Простые вспомогательные методы позволяют узнать размер хранилища и текущую позицию в нем, а также очистить его.

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

Теперь мы подходим к самому интересному: взаимодействию с ресурсами.

Имея заполненный массив storage с прикладными данными, легко "переместить" его в заданный ресурс.

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

Также просто можно прочитать данные из ресурса во внутренний массив storage.

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

Покажем в скрипте Reservoir.mq5, как этим пользоваться.

В первой половине OnStart опишем имя для ресурса-хранилища и объект класса Reservoir, а затем последовательно "упакуем" в этот объект строку, структуру MqlTick и число double. Структура "обернута" в массив из одного элемента, чтобы продемонстрировать в явном виде метод packArray, а кроме того нам затем потребуется сравнить восстановленные данные с исходными, а MQL5 не предоставляет оператор '==' для структур, в связи с чем удобнее будет пользоваться функцией ArrayCompare.

#include <MQL5Book/Reservoir.mqh>
#include <MQL5Book/PRTF.mqh>
   
void OnStart()
{
   const string resource = "::reservoir";
   
   Reservoir res1;
   string message = "message1";     // строка для записи в ресурс
   PRTF(res1.packString(message));
   
   MqlTick tick1[1];                // добавим простую структуру
   SymbolInfoTick(_Symboltick1[0]);
   PRTF(res1.packArray(tick1));
   PRTF(res1.packNumber(DBL_MAX));  // вещественное число
   ...

Когда все необходимые данные "упакованы" в объект, запишем их в ресурс и очистим объект.

   res1.submit(resource);           // создаем ресурс с данными хранилища
   res1.clear();                    // очистка объекта, но не ресурса

Во второй половине OnStart выполним обратные операции чтения данных из ресурса.

   string reply;                    // новая переменная под сообщение
   MqlTick tick2[1];                // новая структура для тика
   double result;                   // новая переменная под число
   
   PRTF(res1.acquire(resource));    // подключаем объект к заданному ресурсу
   PRTF(res1.unpackString(reply));  // читаем строку
   PRTF(res1.unpackArray(tick2));   // читаем простую структуру
   PRTF(res1.unpackNumber(result)); // читаем число
   
   // выводим и сравниваем данные поэлементно
   PRTF(reply);
   PRTF(ArrayCompare(tick1tick2));
   ArrayPrint(tick2);
   PRTF(result == DBL_MAX);
   
   // убеждаемся, что хранилище прочитано до конца
   PRTF(res1.size());
   PRTF(res1.cursor());
   ...

В конце очищаем ресурс, поскольку это тест. В практических задачах MQL-программа, скорее всего, оставит созданный ресурс в памяти, чтобы его могли прочитать другие программы. Напомним, что в иерархии имен ресурсы объявлены вложенными в ту программу, которая их создала. Поэтому для доступа из других программ нужно задать имя ресурса вместе с названием программы и опционально путем (если программа-создатель и программа-читатель находятся в разных папках). Например, для чтения только что созданного ресурса извне подойдет полный путь "\\Scripts\\MQL5Book\\p7\\Reservoir.ex5::reservoir".

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

Поскольку все основные вызовы методов контролируются макросом PRTF, при запуске скрипта мы увидим в журнале подробный "отчет" о ходе действий.

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'

Данные были успешно скопированы в ресурс, а затем восстановлены оттуда.

Программы могут использовать этот подход для обмена объемными данными, которые не помещаются в пользовательские сообщения (события CHARTEVENT_CUSTOM+) — достаточно отсылать в строковом параметре sparam имя ресурса для чтения. Для обратной передачи данных следует создать с ними свой собственный ресурс и отправить ответное сообщение.