- Описание ресурсов с помощью директивы #resource
- Разделяемое использование ресурсов разных MQL-программ
- Ресурсные переменные
- Подключение пользовательских индикаторов как ресурсов
- Динамическое создание ресурсов: ResourceCreate
- Удаление динамических ресурсов: ResourceFree
- Чтение и модификация данных ресурса: ResourceReadImage
- Сохранение изображений в файл: ResourceSave
- Шрифты и вывод текста в графические ресурсы
- Прикладное применение графических ресурсов в трейдинге
Чтение и модификация данных ресурса: 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. Он демонстрирует сразу несколько аспектов работы с графическими ресурсами:
- создание ресурса-картинки из внешнего файла;
- чтение и модификацию данных этой картинки в другом динамически создаваемом ресурсе;
- сохранность созданных ресурсов в памяти терминала между запусками скрипта;
- использование ресурсов в объектах на графике;
- удаление объекта и ресурсов.
Под модификацией изображения в данном конкретном случае понимается инвертирование всех цветов (как наиболее наглядное).
Все перечисленные приемы работы выполняются в три этапа: каждый этап выполняется за один запуск скрипта. Текущий этап скрипт определяет за счета анализа имеющихся ресурсов и объекта:
- При отсутствии требуемых графических ресурсов скрипт создаст их (одно оригинальное изображение и одно инвертированное).
- При наличии ресурсов, но отсутствии графического объекта скрипт создаст объект с двумя изображениями с первого шага для состояний включено/выключено (их можно будет переключать по клику мыши).
- При наличии объекта скрипт удалит объект и ресурсы.
Главная функция скрипта начинается с определения имен для ресурсов и объекта на графике.
void OnStart()
|
Обратите внимание, что мы выбрали имя для исходного ресурса, которое напоминает размещение bmp-файла в стандартной папке Images, однако такого файла нет. Это подчеркивает виртуальную сущность ресурсов и позволяет делать подмены согласно техническим требованиям или в целях затруднения реверсинжиниринга ваших программ.
Следующий вызов ResourceReadImage используется для того, чтобы проверить, существует ли уже ресурс. В начальном состоянии (при первом запуске) мы получим отрицательный результат (false) и начнем первый этап: создаем исходный ресурс из файла "\\Images\\dollar.bmp", а затем инвертируем его в новом ресурсе с суффиксом "_inv".
uint data[], width, height;
|
Исходный код вспомогательной функции ResourceCreateInverted будет представлен ниже.
Если ресурс нашелся (второй запуск), скрипт проверяет наличие объекта и при необходимости создает его, включая установку свойств с ресурсами-картинками в функции ShowBitmap (см. ниже).
else
|
Если же и ресурсы, и объект уже есть на графике, значит мы на заключительном этапе и должны удалить все ресурсы.
else
|
Функция ResourceCreateInverted использует вызов ResourceReadImage для получения массива пикселей, а затем инвертирует в них цвет с помощью оператора '^' (XOR) и операнда со всеми единичными битами в компонентах цвета.
bool ResourceCreateInverted(const string resource, const string inverted)
|
Новый массив data передается в ResourceCreate для создания второго изображения.
Функция ShowBitmap привычным образом создает графический объект (в правом нижнем углу графика) и устанавливает его свойства для включенного и выключенного состояния в, соответственно, оригинальное и инвертированное изображение.
void ShowBitmap(const string name, const string resourceOn, const string resourceOff = NULL)
|
Поскольку только что созданный объект по умолчанию находится в выключенном состоянии, мы сначала увидим инвертированное изображение и сможем его переключить в оригинальное по щелчку мыши. Но напомним, что наш скрипт выполняет действия по шагам, а потому прежде чем изображение появится на графике, скрипт нужно запустить дважды. На всех этапах в журнал выводится текущее состояние и выполняемые действия (вместе с признаком успеха или ошибки).
После первого запуска в журнале появятся такие записи:
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>
|
Как мы знаем, ресурсы строятся на базе массивов типа uint, поэтому опишем такой массив (storage) в классе Reservoir. Туда мы будем складывать все данные, подлежащие последующей записи в ресурс. Текущая позиция в массиве, куда пишутся или откуда считываются данные, хранится в поле offset.
class Reservoir
|
Положить массив данных произвольного типа в storage можно с помощью шаблонного метода packArray. В первой его половине мы преобразуем переданный массив в массив байтов с помощью ByteOverlay.
template<typename T>
|
Во второй половине преобразуем массив байтов в последовательность значений uint, которые записываем в storage по смещению offset. Количество требуемых элементов uint определяется с учетом того, есть ли остаток от деления размера данных в байтах на размер uint: при необходимости добавляем один дополнительный элемент.
const int size = bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
|
Перед самими данными мы пишем размер данных в байтах: это минимальный из возможных протоколов для проверки ошибок при восстановлении данных. В перспективе можно было бы записывать в storage также и typename(T) данных.
Метод возвращает текущую позицию в хранилище после записи.
На основе packArray легко реализовать метод для сохранения строк:
int packString(const string text)
|
Есть также и возможность сохранить отдельное число:
template<typename T>
|
Метод для восстановления массива произвольного типа T из хранилища типа uint "проигрывает" все операции в обратную сторону. При обнаружении нестыковок в читаемом типе и количестве данных с хранилищем метод возвращает 0 (признак ошибки). В штатном режиме возвращается текущая позиции в массиве storage (она всегда больше 0, если что-то успешно прочитано).
template<typename T>
|
Распаковка строк и чисел производится через вызов unpackArray.
int unpackString(string &output)
|
Простые вспомогательные методы позволяют узнать размер хранилища и текущую позицию в нем, а также очистить его.
int size() const
|
Теперь мы подходим к самому интересному: взаимодействию с ресурсами.
Имея заполненный массив storage с прикладными данными, легко "переместить" его в заданный ресурс.
bool submit(const string resource)
|
Также просто можно прочитать данные из ресурса во внутренний массив storage.
bool acquire(const string resource)
|
Покажем в скрипте Reservoir.mq5, как этим пользоваться.
В первой половине OnStart опишем имя для ресурса-хранилища и объект класса Reservoir, а затем последовательно "упакуем" в этот объект строку, структуру MqlTick и число double. Структура "обернута" в массив из одного элемента, чтобы продемонстрировать в явном виде метод packArray, а кроме того нам затем потребуется сравнить восстановленные данные с исходными, а MQL5 не предоставляет оператор '==' для структур, в связи с чем удобнее будет пользоваться функцией ArrayCompare.
#include <MQL5Book/Reservoir.mqh>
|
Когда все необходимые данные "упакованы" в объект, запишем их в ресурс и очистим объект.
res1.submit(resource); // создаем ресурс с данными хранилища
|
Во второй половине OnStart выполним обратные операции чтения данных из ресурса.
string reply; // новая переменная под сообщение
|
В конце очищаем ресурс, поскольку это тест. В практических задачах MQL-программа, скорее всего, оставит созданный ресурс в памяти, чтобы его могли прочитать другие программы. Напомним, что в иерархии имен ресурсы объявлены вложенными в ту программу, которая их создала. Поэтому для доступа из других программ нужно задать имя ресурса вместе с названием программы и опционально путем (если программа-создатель и программа-читатель находятся в разных папках). Например, для чтения только что созданного ресурса извне подойдет полный путь "\\Scripts\\MQL5Book\\p7\\Reservoir.ex5::reservoir".
PrintFormat("Cleaning up local storage '%s'", 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 имя ресурса для чтения. Для обратной передачи данных следует создать с ними свой собственный ресурс и отправить ответное сообщение.