对象显示设置:颜色、样式和边框

可通过各种特性更改对象的外观,本节我们将从颜色、样式、线宽和边框开始探讨这些特性。字体、倾斜度、文本对齐等其他格式特性将在后续章节详细说明。

下表所有特性类型均与整数兼容,因此可通过 ObjectGetIntegerObjectSetInteger 函数进行管理。

标识符

说明

特性类型

OBJPROP_COLOR

对象线条和主要元素的颜色(例如字体或填充色)

color

OBJPROP_STYLE

线条样式

ENUM_LINE_STYLE

OBJPROP_WIDTH

线条粗细(像素)

int

OBJPROP_FILL

用颜色填充对象(适用于 OBJ_RECTANGLE、OBJ_TRIANGLE、OBJ_ELLIPSE、OBJ_CHANNEL、OBJ_STDDEVCHANNEL 和 OBJ_REGRESSION)

bool

OBJPROP_BACK

对象置于背景层

bool

OBJPROP_BGCOLOR

OBJ_EDIT、OBJ_BUTTON、OBJ_RECTANGLE_LABEL 的背景颜色

color

OBJPROP_BORDER_TYPE

矩形面板 OBJ_RECTANGLE_LABEL 的边框类型

ENUM_BORDER_TYPE

OBJPROP_BORDER_COLOR

输入字段 OBJ_EDIT 和按钮 OBJ_BUTTON 的边框颜色

color

与其他大多数带线条对象(如独立垂直线/水平线、趋势线、周期线、通道等)不同,这些对象的 OBJPROP_COLOR 特性定义线条颜色,而对于 OBJ_BITMAP_LABEL 和 OBJ_BITMAP 图像对象,该特性定义边框颜色,而 OBJPROP_STYLE 定义边框绘制类型。

我们在指标一章的 绘图设置章节中已经介绍过用于 OBJPROP_STYLE 的 ENUM_LINE_STYLE 枚举类型。

务必需区分由前景色 OBJPROP_COLOR 执行的填充和背景色 OBJPROP_BGCOLOR 执行的填充。两者分别由不同的对象类型组支持,这些类型已在表中列出。

OBJPROP_BACK 特性需单独说明。需要明确的是,所有对象和指标都默认显示在价格图表上层。用户可以通过以下方式更改整个图表的此行为:进入图表的Setting对话框,然后进入Shared书签中的Chart on top选项。该标志在软件中有对应特性,即 CHART_FOREGROUND 特性(请参阅 图表显示模式章节)。但有时可能需要仅将特定对象移至背景层,而非全部对象。此时,可将这些对象的 OBJPROP_BACK 设为true。在此情况下,如果图表启用了网格和周期分隔线,该对象甚至会被这些分隔线覆盖。

当启用了 OBJPROP_FILL 填充模式时,形状内部的柱线颜色取决于 OBJPROP_BACK 特性。默认情况下,当 OBJPROP_BACK 为false时,与对象重叠的柱线会以 OBJPROP_COLOR 的反色绘制(反色通过将颜色值的所有二进制位取反得到,例如 0xFF0080 的反色为 0x00FF7F)。当 OBJPROP_BACK 为 true时,柱线将保持常规绘制,因为此时对象显示在背景层的图表“下层”(参见下方示例图)。

ENUM_BORDER_TYPE 枚举包含以下元素:

标识符

外观

BORDER_FLAT

平面

BORDER_RAISED

凸起

BORDER_SUNKEN

凹陷

当边框为平面 (BORDER_FLAT) 时,将根据 OBJPROP_COLOR、OBJPROP_STYLE 和 OBJPROP_WIDTH 特性,绘制为对应的颜色、样式和宽度。凸起和凹陷样式会通过 OBJPROP_BGCOLOR 的渐变色模拟边缘的立体倒角效果。

若未设置边框颜色 OBJPROP_BORDER_COLOR(默认值为 clrNone),输入字段将以主色 OBJPROP_COLOR 的线条勾勒边框,而按钮周围则会绘制带有 OBJPROP_BGCOLOR 渐变色调倒角的三维边框。

为测试新特性,可考虑使用ObjectStyle.mq5脚本。在该脚本中,我们将创建 5 个 OBJ_RECTANGLE 类型的矩形,这些对象均基于时间和价格。这些矩形将在窗口整个宽度范围均匀分布,用于高亮显示五个时间段中每个时间段内的最高价 High和最低价Low 之间的区间。对于所有对象,我们将调整并定期更改以下特性:线条颜色、线条样式、线条粗细以及图表底层的填充和显示选项。

我们将再次使用从 Object Selector派生而来的辅助类ObjectBuilder。和上一节不同的是,我们在 ObjectBuilder中添加了析构函数,其中将调用 ObjectDelete

#include <MQL5Book/ObjectMonitor.mqh>
#include <MQL5Book/AutoPtr.mqh>
   
class ObjectBuilderpublic ObjectSelector
{
...
public:
   ~ObjectBuilder()
   {
      ObjectDelete(hostid);
   }
   ...
};

通过这种设计,该辅助类不仅可以配置对象,还可以在脚本运行结束时自动移除这些对象。

OnStart函数中,我们将确定可见柱线数量和第一根柱线的索引,同时计算单个矩形所占的柱体宽度。

#define OBJECT_NUMBER 5
   
void OnStart()
{
   const string name = "ObjStyle-";
   const int bars = (int)ChartGetInteger(0CHART_VISIBLE_BARS);
   const int first = (int)ChartGetInteger(0CHART_FIRST_VISIBLE_BAR);
   const int rectsize = bars / OBJECT_NUMBER;
   ...

我们为对象预留一个智能指针数组,以确保调用 ObjectBuilder析构函数。

   AutoPtr<ObjectBuilderobjects[OBJECT_NUMBER];

定义颜色调色板并创建 5 个矩形对象。

   color colors[OBJECT_NUMBER] = {clrRedclrGreenclrBlueclrMagentaclrOrange};
   
   for(int i = 0i < OBJECT_NUMBER; ++i)
   {
      // find the indexes of the bars that determine the range of prices in the i-th time subrange
      const int h = iHighest(NULL0MODE_HIGHrectsizei * rectsize);
      const int l = iLowest(NULL0MODE_LOWrectsizei * rectsize);
      // create and set up an object in the i-th subrange
      ObjectBuilder *object = new ObjectBuilder(name + (string)(i + 1), OBJ_RECTANGLE);
      object.set(OBJPROP_TIMEiTime(NULL0i * rectsize), 0);
      object.set(OBJPROP_TIMEiTime(NULL0, (i + 1) * rectsize), 1);
      object.set(OBJPROP_PRICEiHigh(NULL0h), 0);
      object.set(OBJPROP_PRICEiLow(NULL0l), 1);
      object.set(OBJPROP_COLORcolors[i]);
      object.set(OBJPROP_WIDTHi + 1);
      object.set(OBJPROP_STYLE, (ENUM_LINE_STYLE)i);
      // save to array
      objects[i] = object;
   }
   ...

此处,为每个对象计算两个锚点的坐标;设置初始颜色、样式和线宽。

接下来,在无限循环中更改对象的特性。当 ScrollLock开启时,动画可以暂停。

   const int key = TerminalInfoInteger(TERMINAL_KEYSTATE_SCRLOCK);
   int pass = 0;
   int offset = 0;
   
   for( ;!IsStopped(); ++pass)
   {
      Sleep(200);
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_SCRLOCK) != keycontinue;
      // change color/style/width/fill/background from time to time
      if(pass % 5 == 0)
      {
         ++offset;
         for(int i = 0i < OBJECT_NUMBER; ++i)
         {
            objects[i][].set(OBJPROP_COLORcolors[(i + offset) % OBJECT_NUMBER]);
            objects[i][].set(OBJPROP_WIDTH, (i + offset) % OBJECT_NUMBER + 1);
            objects[i][].set(OBJPROP_FILLrand() > 32768 / 2);
            objects[i][].set(OBJPROP_BACKrand() > 32768 / 2);
         }
      }
      ChartRedraw();
   }

图表实际效果如下。

具有不同显示设置的 OBJ_RECTANGLE 矩形

具有不同显示设置的 OBJ_RECTANGLE 矩形

最左侧红色矩形启用了填充模式并位于前景层。因此,其内部柱线显示为对比鲜明的亮蓝色(clrAqua,通常也称为 cyan,即clrRed 的反色)。紫色矩形同样有填充,但设置了背景选项,内部柱线以标准方式显示。

请注意,橙色矩形因其较宽的边框线宽且设置为图表顶层显示,会完全覆盖其子范围起点和终点的柱线。

当启用填充时,不会考虑线宽。当边框宽度大于 1 时,部分虚线样式不会被应用。

ObjectShapesDraw

在本节的第二个示例中,回顾我们在第三章学习 OOP 时构思的虚拟绘图程序。当时我们的开发进度停留在:在虚拟绘图方法(名为 draw)中,只能向日志中输出“正在绘制特定形状”的消息。现在通过学习图形对象,我们已具备实际绘图能力。

让我们以 Shapes5stats.mq5 脚本为起点。更新后的版本将命名为 ObjectShapesDraw.mq5

需要说明的是,除了基类Shape外,我们还描述了多个形状类:RectangleEllipseTriangleSquareCircle。所有这些类都成功创建对应类型的图形对象:OBJ_RECTANGLE、OBJ_ELLIPSE、OBJ_TRIANGLE。但存在一些细微差别。

所有指定对象均与时间和价格坐标绑定,而我们的绘图程序采用统一的 X 轴和 Y 轴进行点定位。为此,我们需要以特殊方式配置图表以进行绘制,并调用 ChartXYToTimePrice 函数将屏幕坐标点重新换算为时间和价格值

此外,OBJ_ELLIPSE 和 OBJ_TRIANGLE 对象允许任意旋转(特别是椭圆的长短轴可旋转),而 OBJ_RECTANGLE 的边始终保持水平和垂直方向。为简化示例,我们限制所有形状仅使用标准位置。

从理论上讲,新实现应视为图形对象的演示,而非完整的绘图程序。对于成熟的绘图功能,更合适的做法是使用 图形资源,这样可以避免图形对象带来的限制(因为图形对象一般用于其他用途,例如图表标记)。因此,我们将在后续关于资源章节中重新审视绘图程序的设计。

在新的 Shape类中,我们将移除包含对象坐标的嵌套结构 Pair:该结构原本用于演示 OOP 的多项原则,但现在更简单的做法是将原始的字段描述 int x, y 直接还原到Shape 类中。我们还将添加一个包含对象名的字段。

class Shape
{
   ...
protected:
   int xy;
   color backgroundColor;
   const string type;
   string name;
   
   Shape(int pxint pycolor backstring t) :
      x(px), y(py),
      backgroundColor(back),
      type(t)
   {
   }
   
public:
   ~Shape()
   {
      ObjectDelete(0name);
   }
   ...

name字段是必需的,可用于设置图形对象的特性,也可用于从图表中移除对象,这一操作在析构函数中执行是合乎逻辑的。

由于不同类型的形状需要不同数量的点或特征尺寸,除 draw虚方法外,我们还会在 Shape 接口中添加 setup 方法:

virtual void setup(const int &parameters[]) = 0;

需要说明的是,我们在脚本中已实现了一个嵌套类Shape::Registrator,它原本负责按类型统计形状数量。现在需要赋予它更重要的职责,作为形状工厂来运作。“工厂”类或方法的优势在于能够以统一的方式创建不同类的对象。

为此,我们向 Registrator中添加一个创建形状的方法,其参数包括:第一个点的坐标(必需)、颜色以及一个额外参数数组(每个形状都能够按照自己的规则解析该数组,且未来可从中读取或写入文件)。

virtual Shape *create(const int pxconst int pyconst color back,
         const int &parameters[]) = 0;

该方法是抽象虚方法,因为特定类型的形状只能通过Shape派生类中描述的派生注册类来创建。为简化派生日志记录类的编写工作,我们引入了一个模板类 MyRegistrator,其包含适用于所有场景的 create 方法实现。

template<typename T>
class MyRegistrator : public Shape::Registrator
{
public:
   MyRegistrator() : Registrator(typename(T))
   {
   }
   
   virtual Shape *create(const int pxconst int pyconst color back,
      const int &parameters[]) override
   {
      T *temp = new T(pxpyback);
      temp.setup(parameters);
      return temp;
   }
};

在此方法中,我们调用某个先前未知形状 T 的构造函数,通过调用 setup对其进行调整,并将实例返回给调用代码。

以下是在 Rectangle类中的使用方式,该类有两个用于宽度和高度的额外参数。

class Rectangle : public Shape
{
   static MyRegistrator<Rectangler;
   
protected:
   int dxdy// dimensions (width, height)
   
   Rectangle(int pxint pycolor backstring t) :
      Shape(pxpybackt), dx(1), dy(1)
   {
   }
   
public:
   Rectangle(int pxint pycolor back) :
      Shape(pxpybacktypename(this)), dx(1), dy(1)
   {
      name = typename(this) + (string)r.increment();
   }
   
   virtual void setup(const int &parameters[]) override
   {
      if(ArraySize(parameters) < 2)
      {
         Print("Insufficient parameters for Rectangle");
         return;
      }
      dx = parameters[0];
      dy = parameters[1];
   }
   ...
};
   
static MyRegistrator<RectangleRectangle::r;

创建形状时,其名称不仅包含类名(typename),还包含通过 r.increment() 调用计算得出的实例序号。

其他形状类的描述方式类似。

现在我们需要研究 Rectangle类的draw 方法。在该方法中,我们使用 ChartXYToTimePrice将一对点 (x,y) 和 (x + dx, y + dy) 转换为时间/价格坐标,并创建一个 OBJ_RECTANGLE 对象。

   void draw() override
   {
      // Print("Drawing rectangle");
      int subw;
      datetime t;
      double p;
      ChartXYToTimePrice(0xysubwtp);
      ObjectCreate(0nameOBJ_RECTANGLE0tp);
      ChartXYToTimePrice(0x + dxy + dysubwtp);
      ObjectSetInteger(0nameOBJPROP_TIME1t);
      ObjectSetDouble(0nameOBJPROP_PRICE1p);
   
      ObjectSetInteger(0nameOBJPROP_COLORbackgroundColor);
      ObjectSetInteger(0nameOBJPROP_FILLtrue);
   }

当然,不要忘记将颜色设置为 OBJPROP_COLOR,将填充设置为 OBJPROP_FILL。

对于 Square类,实际上不需要做任何更改:只需将 dxdy 设置为相等即可。

对于 Ellipse类,两个额外选项 dxdy 决定了相对于中心 (x,y) 绘制的短半径和长半径。相应地,在 draw方法中,我们将计算 3 个锚点并创建一个 OBJ_ELLIPSE 对象。

class Ellipse : public Shape
{
   static MyRegistrator<Ellipser;
protected:
   int dxdy// large and small radii 
   ...
public:
   void draw() override
   {
      // Print("Drawing ellipse");
      int subw;
      datetime t;
      double p;
      
      // (x, y) center
      // p0: x + dx, y
      // p1: x - dx, y
      // p2: x, y + dy
      
      ChartXYToTimePrice(0x + dxysubwtp);
      ObjectCreate(0nameOBJ_ELLIPSE0tp);
      ChartXYToTimePrice(0x - dxysubwtp);
      ObjectSetInteger(0nameOBJPROP_TIME1t);
      ObjectSetDouble(0nameOBJPROP_PRICE1p);
      ChartXYToTimePrice(0xy + dysubwtp);
      ObjectSetInteger(0nameOBJPROP_TIME2t);
      ObjectSetDouble(0nameOBJPROP_PRICE2p);
      
      ObjectSetInteger(0nameOBJPROP_COLORbackgroundColor);
      ObjectSetInteger(0nameOBJPROP_FILLtrue);
   }
};
   
static MyRegistrator<EllipseEllipse::r;

Circle 是长半径和短半径相等的特殊椭圆。

最后一点,现阶段仅支持等边三角形:其边长包含在额外字段dx中。我们建议你自行查阅源代码中的 draw方法。

新脚本将延续之前的功能,随机生成指定数量的形状。它们由 addRandomShape函数创建。

Shape *addRandomShape()
{
   const int w = (int)ChartGetInteger(0CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0CHART_HEIGHT_IN_PIXELS);
   
   const int n = random(Shape::Registrator::getTypeCount());
   
   int cx = 1 + w / 4 + random(w / 2), cy = 1 + h / 4 + random(h / 2);
   int clr = ((random(256) << 16) | (random(256) << 8) | random(256));
   int custom[] = {1 + random(w / 4), 1 + random(h / 4)};
   return Shape::Registrator::get(n).create(cxcyclrcustom);
}

在该函数中,我们可以看到工厂方法 create的用法,该方法通过随机选择的寄存器对象,带有数字n。如果决定后续添加其他形状类,也无需更改生成逻辑中的任何内容。

所有形状都均位于窗口中央,且尺寸不超过窗口的四分之一。

接下来需要直接研究 addRandomShape函数的调用,以及前文提及的特殊时间表设置。

为实现屏幕上点的正方形呈现,可设置 CHART_SCALEFIX_11 模式。此外,我们将沿时间轴选择最密集(压缩)的刻度 CHART_SCALE (0),因为其中一个柱线占用 1 个水平像素(最高精度)。最后,通过将 CHART_SHOW 设置为 false来禁用图表本身的显示。

void OnStart()
{
   const int scale = (int)ChartGetInteger(0CHART_SCALE);
   ChartSetInteger(0CHART_SCALEFIX_11true);
   ChartSetInteger(0CHART_SCALE0);
   ChartSetInteger(0CHART_SHOWfalse);
   ChartRedraw();
   ...

为存储形状,我们将预留一个智能指针数组,并用随机形状填充该数组。

#define FIGURES 21
...
void OnStart()
{
   ...
   AutoPtr<Shapeshapes[FIGURES];
   
   for(int i = 0i < FIGURES; ++i)
   {
      Shape *shape = shapes[i] = addRandomShape();
      shape.draw();
   }
   
   ChartRedraw();
   ...

然后,运行一个无限循环,直到用户停止脚本,在循环中通过 move方法微调所有形状的位置。

   while(!IsStopped())
   {
      Sleep(250);
      for(int i = 0i < FIGURES; ++i)
      {
         shapes[i][].move(random(20) - 10random(20) - 10);
         shapes[i][].draw();
      }
      ChartRedraw();
   }
   ...

最后,我们将恢复图表设置。

   // it's not enough to disable CHART_SCALEFIX_11, you need CHART_SCALEFIX
   ChartSetInteger(0CHART_SCALEFIXfalse);
   ChartSetInteger(0CHART_SCALEscale);
   ChartSetInteger(0CHART_SHOWtrue);
}

以下截图展示了绘制形状后的图表可能呈现的效果:

图表形状对象
图表形状对象

对象绘制的特点是在重叠区域产生颜色“叠加”效果。

由于 Y 轴采用自上而下的方向,所有三角形均呈现倒置状态,但这个问题不是很严重,因为我们最终将基于资源重构绘图程序。