读取和修改资源数据: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()
|
请注意,我们为原始资源选择了一个名称,该名称看起来像标准 Images 文件夹中 bmp 文件的位置,但实际上并不存在这样的文件。这强调了资源的虚拟特性,并允许进行替换以满足技术要求,或者增加程序被逆向工程的难度。
接下来的 ResourceReadImage 调用用于检查资源是否已存在。在初始状态下(首次运行时),我们将得到否定结果 (false),然后开始第一步:我们从文件 "\\Images\\dollar.bmp" 创建原始资源,然后在一个带有 "_inv" 后缀的新资源中将其反转。
uint data[], width, height;
|
辅助函数 ResourceCreateInverted 的源代码将在下面给出。
如果找到资源(第二次运行时),脚本会检查对象是否存在,并在必要时创建它,包括在 ShowBitmap 函数中(见下文)设置带有图像资源的属性。
else
|
如果资源和对象都已在图表上,那么我们就处于最后阶段,必须移除所有资源。
else
|
ResourceCreateInverted 函数使用 ResourceReadImage 调用来获取像素数组,然后使用 '^'(异或)运算符和一个在颜色分量中所有位都为 1 的操作数来反转其中的颜色。
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 类型数组的基础上的,因此我们在 Reservoir 类中描述了这样一个数组 (storage)。我们将把所有随后要写入资源的数据添加到那里。数组中写入或读取数据的当前位置存储在 offset 字段中。
class Reservoir
|
要将任意类型的数据数组放入 storage 中,可以使用模板方法 packArray。在其前半部分,我们使用 ByteOverlay 将传递的数组转换为字节数组。
template<typename T>
|
在其后半部分,我们将字节数组转换为 uint 值的序列,这些值带有 offset 写入 storage 中。所需 uint 元素的数量是通过考虑数据大小(以字节为单位)除以 uint 大小后是否有余数来确定的:我们可以选择性地添加一个额外的元素。
const int size = bytesize / sizeof(uint) + (bool)(bytesize % sizeof(uint));
|
在数据本身之前,我们写入数据的字节大小:这是用于恢复数据时进行错误检查的最小可行协议。将来,也可以将 typename(T) 数据写入 storage 中。
该方法返回写入后存储中的当前位置。
基于 packArray,很容易实现一个保存字符串的方法:
int packString(const string text)
|
还有一个存储单个数字的选项:
template<typename T>
|
一个从 uint 类型存储中恢复任意类型 T 数组的方法,会反向“回放”所有操作。如果在可读类型和数据量与存储不一致时发现问题,该方法返回 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); // create a resource with storage data
|
在 OnStart 的后半部分,让我们执行从资源中读取数据的反向操作。
string reply; // new variable for message
|
最后,我们清理资源,因为这是一个测试。在实际任务中,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 字符串参数中发送要读取的资源名称即可。要回传数据,请创建你自己的资源,在其中包含该数据,并发送响应消息。