保存图像到文件:ResourceSave

借助 MQL5 API,你可以使用 ResourceSave 函数将资源写入 BMP 文件。该框架目前仅支持图像资源。

bool ResourceSave(const string resource, const string filename)

resourcefilename 参数分别指定资源和文件的名称。资源名称必须以 "::" 开头。文件名可以包含相对于 MQL5/Files 文件夹的路径。如有必要,该函数将创建所有中间子目录。如果指定的文件存在,它将被覆盖。

函数成功时返回 true

为了测试此函数的操作,最好创建一个原始图像。我们正好有适合这个的图像。

作为 OOP 学习的一部分,在 类和接口一章中,我们开始了一系列关于图形形状的示例:从 类定义 一节中的第一个版本 Shapes1.mq5嵌套类型中的最后一个版本 Shapes6.mq5。那时我们还无法进行绘图,直到关于图形对象的章节,我们才能够在 ObjectShapesDraw.mq5脚本中实现可视化。现在,在学习了图形资源之后,是时候进行另一次“升级”了。

在新版本的 ResourceShapesDraw.mq5 脚本中,我们将绘制这些形状。为了更容易地与先前版本比较更改,我们将保留相同的形状集:矩形、正方形、椭圆形、圆形和三角形。这样做是为了举例说明,而不是因为有什么限制我们绘图:相反,在扩展形状集、视觉效果和标签方面存在潜力。我们将通过几个示例来了解这些特性,从当前的这个开始。但是,请注意,本书的范围无法演示所有应用。

在生成并绘制形状后,我们将生成的资源保存到文件中。

形状类层次结构的基础是具有 draw 方法的 Shape 类。

class Shape
{
public:
   ...
   virtual void draw() = 0;
   ...
}

在派生类中,它是基于图形对象实现的,通过调用 ObjectCreate 并随后使用 ObjectSet 函数设置对象。这种绘图的共享画布是图表本身。

现在我们需要根据特定的形状在某个共享资源中绘制像素。最好将公共资源和修改其中像素的方法分配到一个单独的类中,或者更好的是,一个接口中。

抽象实体将使我们不必与创建和配置资源的方法产生联系。特别是,我们的下一个实现会将资源放置在 OBJ_BITMAP_LABEL 对象中(正如我们在本章中已经做过的那样),而对于某些情况,可能只需要在内存中生成图像并保存到磁盘而无需绘制(许多交易者喜欢定期捕获图表状态)。

我们称这个接口为 Drawing

interface Drawing
{
   void point(const float x1const float y1const uint pixel);
   void line(const int x1const int y1const int x2const int y2const color clr);
   void rect(const int x1const int y1const int x2const int y2const color clr);
};

这里只有三个最基本的绘图方法,对于本例来说已经足够了。

point 方法是公共的(这使得可以绘制一个单独的点),但在某种意义上它是低级的,因为所有其他方法都将通过它来实现。这就是为什么其中的坐标是真实的,并且像素的内容是 uint 类型的现成值。这将允许在必要时应用各种抗锯齿算法,以使形状看起来不会因像素化而出现阶梯状。这里我们不讨论这个问题。

考虑到接口,Shape::draw 方法变为以下形式:

virtual void draw(Drawing *drawing) = 0;

然后,在 Rectangle 类中,很容易将矩形的绘制委托给新的接口。

class Rectangle : public Shape
{
protected:
   int dxdy// size (width, height)
   ...
public:
   void draw(Drawing *drawingoverride
   {
 // x, y - anchor point (center) in Shape
      drawing.rect(x – dx / 2y – dy / 2x + dx / 2y + dy / 2backgroundColor);
   }
};

绘制椭圆需要更多的努力。

class Ellipse : public Shape
{
protected:
   int dxdy// large and small radii
   ...
public:
   void draw(Drawing *drawingoverride
   {
      // (x, y) - center
      const int hh = dy * dy;
      const int ww = dx * dx;
      const int hhww = hh * ww;
      int x0 = dx;
      int step = 0;
      
      // main horizontal diameter
      drawing.line(x - dxyx + dxybackgroundColor);
      
      // horizontal lines in the upper and lower half, symmetrically decreasing in length
      for(int j = 1j <= dyj++)
      {
         for(int x1 = x0 - (step - 1); x1 > 0; --x1)
         {
            if(x1 * x1 * hh + j * j * ww <= hhww)
            {
               step = x0 - x1;
               break;
            }
         }
         x0 -= step;
         drawing.line(x - x0y - jx + x0y - jbackgroundColor);
         drawing.line(x - x0y + jx + x0y + jbackgroundColor);
      }
   }
};

最后,对于三角形,渲染实现如下。

class Trianglepublic Shape
{
protected:
   int dx;  // one size, because triangles are equilateral 
   ...
public:
   virtual void draw(Drawing *drawingoverride
   {
      // (x, y) - center
      // R = a * sqrt(3) / 3
      // p0: x, y + R
      // p1: x - R * cos(30), y - R * sin(30)
      // p2: x + R * cos(30), y - R * sin(30)
      // Pythagorean height: dx * dx = dx * dx / 4 + h * h
      // sqrt(dx * dx * 3/4) = h
      const double R = dx * sqrt(3) / 3;
      const double H = sqrt(dx * dx * 3 / 4);
      const double angle = H / (dx / 2);
      
      // main vertical line (triangle height)
      const int base = y + (int)(R - H);
      drawing.line(xy + (int)RxbasebackgroundColor);
      
      // smaller vertical lines left and right, symmetrical
      for(int j = 1j <= dx / 2; ++j)
      {
         drawing.line(x - jy + (int)(R - angle * j), x - jbasebackgroundColor);
         drawing.line(x + jy + (int)(R - angle * j), x + jbasebackgroundColor);
      }
   }
};

现在,我们转向派生自 Drawing 接口的 MyDrawing 类。正是 MyDrawing 必须在形状中接口方法调用的指导下,确保某个资源显示在位图中。因此,该类描述了图形对象 (object) 和资源 (sheet) 名称的变量,以及用于存储图像的 uint 类型 data 数组。此外,我们移动了先前在 OnStart 处理程序中声明的形状数组 shapes。由于 MyDrawing 负责绘制所有形状,因此最好在这里管理它们的集合。

class MyDrawingpublic Drawing
{
   const string object;     // object with bitmap
   const string sheet;      // resource
   uint data[];             // pixels
   int widthheight;       // dimensions
   AutoPtr<Shapeshapes[]; // figures/shapes
   const uint bg;           // background color
   ...

在构造函数中,我们为整个图表的大小创建一个图形对象,并为 data 数组分配内存。画布用零(表示“黑色透明”)或 background 参数中传递的任何值填充,然后基于它创建一个资源。默认情况下,资源名称以字母 'D' 开头并包含当前图表的 ID,但你可以指定其他名称。

public:
   MyDrawing(const uint background = 0const string s = NULL) :
      object((s == NULL ? "Drawing" : s)),
      sheet("::" + (s == NULL ? "D" + (string)ChartID() : s)), bg(background)
   {
      width = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
      height = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
      ArrayResize(datawidth * height);
      ArrayInitialize(databackground);
   
      ResourceCreate(sheetdatawidthheight00widthCOLOR_FORMAT_ARGB_NORMALIZE);
      
      ObjectCreate(0objectOBJ_BITMAP_LABEL000);
      ObjectSetInteger(0objectOBJPROP_XDISTANCE0);
      ObjectSetInteger(0objectOBJPROP_YDISTANCE0);
      ObjectSetInteger(0objectOBJPROP_XSIZEwidth);
      ObjectSetInteger(0objectOBJPROP_YSIZEheight);
      ObjectSetString(0objectOBJPROP_BMPFILEsheet);
   }

调用代码可以使用 resource 方法找出资源的名称。

   string resource() const
   {
      return sheet;
   }

资源和对象在析构函数中被移除。

   ~MyDrawing()
   {
      ResourceFree(sheet);
      ObjectDelete(0object);
   }

push 方法填充形状数组。

   Shape *push(Shape *shape)
   {
      shapes[EXPAND(shapes)] = shape;
      return shape;
   }

draw 方法绘制形状。它只是在循环中调用每个形状的 draw 方法,然后更新资源和图表。

   void draw()
   {
      for(int i = 0i < ArraySize(shapes); ++i)
      {
         shapes[i][].draw(&this);
      }
      ResourceCreate(sheetdatawidthheight00widthCOLOR_FORMAT_ARGB_NORMALIZE);
      ChartRedraw();
   }

下面是最重要的方法,它们是 Drawing 接口的方法,并且实际实现绘图。

我们从 point 方法开始,我们暂时以简化形式呈现它(我们稍后会处理改进)。

   virtual void point(const float x1const float y1const uint pixeloverride
   {
      const int x_main = (int)MathRound(x1);
      const int y_main = (int)MathRound(y1);
      const int index = y_main * width + x_main;
      if(index >= 0 && index < ArraySize(data))
      {
         data[index] = pixel;
      }
   }

基于 point,很容易实现直线绘制。当起点和终点的坐标在某个维度上匹配时,我们使用 rect 方法进行绘制,因为直线是单位厚度矩形的退化情况。

   virtual void line(const int x1const int y1const int x2const int y2const color clroverride
   {
      if(x1 == x2rect(x1y1x1y2clr);
      else if(y1 == y2rect(x1y1x2y1clr);
      else
      {
         const uint pixel = ColorToARGB(clr);
         double angle = 1.0 * (y2 - y1) / (x2 - x1);
         if(fabs(angle) < 1// step along the axis with the largest distance, x
         {
            const int sign = x2 > x1 ? +1 : -1;
            for(int i = 0i <= fabs(x2 - x1); ++i)
            {
               const float p = (float)(y1 + sign * i * angle);
               point(x1 + sign * ippixel);
            }
         }
         else // or y-step
         {
            const int sign = y2 > y1 ? +1 : -1;
            for(int i = 0i <= fabs(y2 - y1); ++i)
            {
               const float p = (float)(x1 + sign * i / angle);
               point(py1 + sign * ipixel);
            }
         }
      }
   }

这是 rect 方法。

   virtual void rect(const int x1const int y1const int x2const int y2const color clroverride
   {
      const uint pixel = ColorToARGB(clr);
      for(int i = fmin(x1x2); i <= fmax(x1x2); ++i)
      {
         for(int j = fmin(y1y2); j <= fmax(y1y2); ++j)
         {
            point(ijpixel);
         }
      }
   }

现在我们需要修改 OnStart 处理程序,脚本就准备好了。

首先,我们设置图表(隐藏所有元素)。理论上,这不是必需的:这样做是为了与原型脚本匹配。

void OnStart()
{
   ChartSetInteger(0CHART_SHOWfalse);
   ...

接下来,我们描述 MyDrawing 类的对象,生成预定数量的随机形状(这里一切保持不变,包括 addRandomShape 生成器和等于 21 的 FIGURES 宏),在资源中绘制它们,并在图表上的对象中显示它们。

   MyDrawing raster;
   
   for(int i = 0i < FIGURES; ++i)
   {
      raster.push(addRandomShape());
   }
   
   raster.draw(); // display the initial state
   ...

ObjectShapesDraw.mq5 示例中,我们启动了一个无限循环,在其中随机移动图形块。我们在这里重复这个技巧。这里我们需要添加 MyDrawing 类,因为形状数组存储在其中。我们编写一个简单的 shake 方法。

class MyDrawingpublic Drawing
{
public:
   ...
   void shake()
   {
      ArrayInitialize(databg);
      for(int i = 0i < ArraySize(shapes); ++i)
      {
         shapes[i][].move(random(20) - 10random(20) - 10);
      }
   }
   ...
};

然后,在 OnStart 中,我们可以在循环中使用新方法,直到用户停止动画。

void OnStart()
{
   ...
   while(!IsStopped())
   {
      Sleep(250);
      raster.shake();
      raster.draw();
   }
   ...
}

至此,先前示例的功能实际上已经重复了。但是我们需要添加将图像保存到文件的功能。所以,我们添加一个输入参数 SaveImage

input bool SaveImage = false;

当它设置为 true 时,检查 ResourceSave 函数的性能。

void OnStart()
{
   ...
   if(SaveImage)
   {
      const string filename = "temp.bmp";
      if(ResourceSave(raster.resource(), filename))
      {
         Print("Bitmap image saved: "filename);
      }
      else
      {
         Print("Can't save image "filename", "E2S(_LastError));
      }
   }
}

另外,既然我们谈论的是输入变量,让用户选择一个背景并将结果值传递给 MyDrawing 构造函数。

input color BackgroundColor = clrNONE;
void OnStart()
{
   ...
   MyDrawing raster(BackgroundColor != clrNONE ? ColorToARGB(BackgroundColor) : 0);
   ...
}

那么,一切都为第一次测试做好了准备。

如果运行 ResourceShapesDraw.mq5 脚本,图表将形成如下图所示的图像。

带有随机形状集的资源位图

带有随机形状集的资源位图

将此图像与我们在 ObjectShapesDraw.mq5示例中看到的图像进行比较时,结果表明我们的新渲染方式与终端显示对象的方式有所不同。尽管形状和颜色是正确的,但形状重叠的地方以不同的方式指示。

我们的脚本用指定的颜色绘制形状,按照它们在数组中出现的顺序将它们叠加在一起。后面的形状会覆盖前面的形状。另一方面,终端在重叠的地方应用某种颜色混合(反转)。

两种方法都有其存在的理由,这里没有错误。然而,在绘图时是否可以达到类似的效果?

我们完全控制绘图过程,因此不仅可以应用终端的效果,还可以应用任何效果。

除了原始的简单绘图方式外,让我们再实现几种模式。所有这些都总结在 COLOR_EFFECT 枚举中。

enum COLOR_EFFECT
{
   PLAIN,         // simple drawing with overlap (default)
   COMPLEMENT,    // draw with a complementary color (like in the terminal) 
   BLENDING_XOR,  // mixing colors with XOR '^'
   DIMMING_SUM,   // "darken" colors with '+'
   LIGHTEN_OR,    // "lighten" colors with '|'
};

我们添加一个输入变量来选择模式。

input COLOR_EFFECT ColorEffect = PLAIN;

我们在 MyDrawing 类中支持这些模式。首先,我们描述相应的字段和方法。

class MyDrawingpublic Drawing
{
   ...
   COLOR_EFFECT xormode;
   ...
public:
   void setColorEffect(const COLOR_EFFECT x)
   {
      xormode = x;
   }
   ...

然后,我们改进 point 方法。

   virtual void point(const float x1const float y1const uint pixeloverride
   {
      ...
      if(index >= 0 && index < ArraySize(data))
      {
         switch(xormode)
         {
         case COMPLEMENT:
            data[index] = (pixel ^ (1 - data[index])); // blending with complementary color
            break;
         case BLENDING_XOR:
            data[index] = (pixel & 0xFF000000) | (pixel ^ data[index]); // direct mixing (XOR)
            break;
         case DIMMING_SUM:
            data[index] =  (pixel + data[index]); // "darkening" (SUM)
            break;
         case LIGHTEN_OR:
            data[index] =  (pixel & 0xFF000000) | (pixel | data[index]); // "lightening" (OR)
            break;
         case PLAIN:
         default:
            data[index] = pixel;
         }
      }
   }

你可以尝试以不同的模式运行脚本并比较结果。不要忘记自定义背景的功能。这是一个颜色变亮效果的示例。

带有颜色变亮混合的图形图像

带有颜色变亮混合的形状图像

为了直观地看到效果的差异,你可以关闭颜色随机化和形状移动。对象的标准重叠方式对应于 COMPLEMENT 常量。

作为最后的实验,启用 SaveImage 选项。在 OnStart 处理程序中,当生成带有图像的文件名时,我们现在使用当前模式的名称。我们需要在文件中获取图表上图像的副本。

   ...
   if(SaveImage)
   {
      const string filename = EnumToString(ColorEffect) + ".bmp";
      if(ResourceSave(raster.resource(), filename))
      ...

对于我们界面中更复杂的图形构造,Drawing 可能不够用。因此,可以使用 MetaTrader 5 附带的或 mql5.com 代码库中可用的现成绘图类。特别是,请查看 MQL5/Include/Canvas/Canvas.mqh 文件。