读取和修改资源数据: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 数组,widthheight 参数将分别间接接收宽度和高度,即数组的大小 (width*height)。另外,只有当图像存储在资源中时,widthheight 才有意义。数组必须是动态或固定的,但必须有足够的大小。否则,我们将收到 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";
   ...

请注意,我们为原始资源选择了一个名称,该名称看起来像标准 Images 文件夹中 bmp 文件的位置,但实际上并不存在这样的文件。这强调了资源的虚拟特性,并允许进行替换以满足技术要求,或者增加程序被逆向工程的难度。

接下来的 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")); // try "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 调用来获取像素数组,然后使用 '^'(异或)运算符和一个在颜色分量中所有位都为 1 的操作数来反转其中的颜色。

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 类型数组的基础上的,因此我们在 Reservoir 类中描述了这样一个数组 (storage)。我们将把所有随后要写入资源的数据添加到那里。数组中写入或读取数据的当前位置存储在 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: check for overflow
      uchar buffer[];
      ArrayResize(bufferbytesize);
      for(int i = 0i < ArraySize(data); ++i)
      {
         ByteOverlay<Toverlay(data[i]);
         ArrayCopy(bufferoverlay.bufferi * sizeof(T));
      }
      ...

在其后半部分,我们将字节数组转换为 uint 值的序列,这些值带有 offset 写入 storage 中。所需 uint 元素的数量是通过考虑数据大小(以字节为单位)除以 uint 大小后是否有余数来确定的:我们可以选择性地添加一个额外的元素。

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

在数据本身之前,我们写入数据的字节大小:这是用于恢复数据时进行错误检查的最小可行协议。将来,也可以将 typename(T) 数据写入 storage 中。

该方法返回写入后存储中的当前位置。

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

一个从 uint 类型存储中恢复任意类型 T 数组的方法,会反向“回放”所有操作。如果在可读类型和数据量与存储不一致时发现问题,该方法返回 0(错误标志)。在正常模式下,返回 storage 数组中的当前位置(如果成功读取了某些内容,它总是大于 0)。

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

解包字符串和数字是通过调用 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";     // 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
   ...

当所有必要的数据都“打包”到对象中后,将其写入资源并清除该对象。

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

OnStart 的后半部分,让我们执行从资源中读取数据的反向操作。

   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());
   ...

最后,我们清理资源,因为这是一个测试。在实际任务中,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 字符串参数中发送要读取的资源名称即可。要回传数据,请创建你自己的资源,在其中包含该数据,并发送响应消息。

该网站使用cookies。了解有关我们Cookies政策的更多信息。