对象特性访问函数概述

对象具有多种类型的特性,这些特性可通过 ObjectGetObjectSet 函数进行读取和设置。我们知道,这一原则已在图表中应用(请参阅 用于处理完整图表特性集的函数概述 章节)。

所有此类函数的前三个参数均为图表标识符、对象名称和特性标识符,其中特性标识符必须是 ENUM_OBJECT_PROPERTY_INTEGER、ENUM_OBJECT_PROPERTY_DOUBLE 或 ENUM_OBJECT_PROPERTY_STRING 枚举的成员。我们将在后续章节逐步研究具体特性。完整的特性对照表可在 MQL5 文档的 对象特性页面上找到。

需要注意的是,所有三个枚举中的特性标识符互不重叠,这使得我们可以将它们的联合处理合并为单个统一代码。我们将在示例中运用这一点。

某些特性为只读,并将标记为 "r/o"(只读)。

与绘图 API 的情况类似,特性读取函数具有简写和完整两种形式:简写形式直接返回请求的值,完整形式返回布尔值(true表示成功,false 表示发生错误),实际值则通过引用传递的最后一个参数返回。调用简写形式时,必须通过内置变量 _LastError检查是否发生错误。

访问某些特性时,必须指定一个额外参数 (modifier),用于在特性为多值时指示值的编号或级别。例如,如果一个对象有多个锚点,则修饰符可用于选择特定的锚点。

以下是用于读取和写入整数型特性的函数原型。需特别注意,这些函数的值类型为 long,因此不仅允许存储 intlong 类型的特性,还可以存储 boolcolordatetime 类型的特性以及各种枚举(见下文)。

bool ObjectSetInteger(long chartId, const string name, ENUM_OBJECT_PROPERTY_INTEGER property, long value)

bool ObjectSetInteger(long chartId, const string name, ENUM_OBJECT_PROPERTY_INTEGER property, int modifier, long value)

long ObjectGetInteger(long chartId, const string name, ENUM_OBJECT_PROPERTY_INTEGER property, int modifier = 0)

bool ObjectGetInteger(long chartId, const string name, ENUM_OBJECT_PROPERTY_INTEGER property, int modifier, long &value)

实数型特性的函数说明类似。

bool ObjectSetDouble(long chartId, const string name, ENUM_OBJECT_PROPERTY_DOUBLE property, double value)

bool ObjectSetDouble(long chartId, const string name, ENUM_OBJECT_PROPERTY_DOUBLE property, int modifier, double value)

double ObjectGetDouble(long chartId, const string name, ENUM_OBJECT_PROPERTY_DOUBLE property, int modifier = 0)

bool ObjectGetDouble(long chartId, const string name, ENUM_OBJECT_PROPERTY_DOUBLE property, int modifier, double &value)

最后,字符串同样存在四个相同的函数。

bool ObjectSetString(long chartId, const string name, ENUM_OBJECT_PROPERTY_STRING property, const string value)

bool ObjectSetString(long chartId, const string name, ENUM_OBJECT_PROPERTY_STRING property, int modifier, const string value)

string ObjectGetString(long chartId, const string name, ENUM_OBJECT_PROPERTY_STRING property, int modifier = 0)

bool ObjectGetString(long chartId, const string name, ENUM_OBJECT_PROPERTY_STRING property, int modifier, string &value)

为提升性能,所有设置对象特性的函数(ObjectSetIntegerObjectSetDoubleObjectSetString)均为异步函数,本质上是向图表发送修改对象的命令。当这些函数成功执行时,命令将被存入图表的共享事件队列,并返回结果true表示操作成功。如果发生错误,函数将返回 false,,此时必须通过 _LastError 变量检查错误代码。

对象特性的更改存在一定延迟,在图表事件队列处理期间完成。要强制更新图表上对象的外观和特性(尤其是在一次性更改多个对象后),可使用 ChartRedraw 函数。

获取图表特性的函数(ObjectGetIntegerObjectGetDoubleObjectGetString)是同步的,这意味着调用代码会等待这些函数的执行结果。在这种情况下,图表队列中的所有命令均被执行,以获取最新特性值。

让我们回到 删除对象脚本的示例,更准确地说,是其新版本ObjectCleanup2.mq5。回顾在CustomDeleteAllObjects函数中,我们希望实现根据对象特性选择对象的功能。假设这些特性应该是颜色和锚点。要获取这些特性,可使用ObjectGetInteger函数以及 ENUM_OBJECT_PROPERTY_INTEGER 枚举中的一对元素:OBJPROP_COLOR 和 OBJPROP_ANCHOR。我们将在后续详细探讨这些内容。

基于此,代码需补充以下检查逻辑(为简化演示,此处颜色和锚点分别使用clrRed和 ANCHOR_TOP 常量表示,实际应用中我们会提供对应的输入变量)。

int CustomDeleteAllObjects(const long chartconst string prefix,
   const int window = -1const int type = -1)
{
   int count = 0;
   
   for(int i = ObjectsTotal(chartwindowtype) - 1i >= 0; --i)
   {
      const string name = ObjectName(chartiwindowtype);
      // condition on the name and additional properties, such as color and anchor point
      if((StringLen(prefix) == 0 || StringFind(nameprefix) == 0)
         && ObjectGetInteger(0nameOBJPROP_COLOR) == clrRed
         && ObjectGetInteger(0nameOBJPROP_ANCHOR) == ANCHOR_TOP)
      {
         count += ObjectDelete(chartname);
      }
   }
   return count;
}

请注意包含 ObjectGetInteger的代码行。

这些代码较长且存在重复,因为特定特性与已知类型的ObjectGet函数绑定。此外,随着条件数量的增加,重复填写图表 ID 和对象名称会显得冗余。

为简化代码结构,我们将采用在 图表显示模式章节中ChartModeMonitor.mqh文件中测试过的技术。这样做的意义在于构建一个中间类,通过方法重载实现对所有类型特性的读写操作。我们将新的头文件命名为 ObjectMonitor.mqh

ObjectProxy类的结构与用于图表ChartModeMonitorInterface 类高度相似。主要区别在于它包含用于设置和获取图表 ID 及对象名称的虚方法。

class ObjectProxy
{
public:
   long get(const ENUM_OBJECT_PROPERTY_INTEGER propertyconst int modifier = 0)
   {
      return ObjectGetInteger(chart(), name(), propertymodifier);
   }
   double get(const ENUM_OBJECT_PROPERTY_DOUBLE propertyconst int modifier = 0)
   {
      return ObjectGetDouble(chart(), name(), propertymodifier);
   }
   string get(const ENUM_OBJECT_PROPERTY_STRING propertyconst int modifier = 0)
   {
      return ObjectGetString(chart(), name(), propertymodifier);
   }
   bool set(const ENUM_OBJECT_PROPERTY_INTEGER propertyconst long value,
      const int modifier = 0)
   {
      return ObjectSetInteger(chart(), name(), propertymodifiervalue);
   }
   bool set(const ENUM_OBJECT_PROPERTY_DOUBLE propertyconst double value,
      const int modifier = 0)
   {
      return ObjectSetDouble(chart(), name(), propertymodifiervalue);
   }
   bool set(const ENUM_OBJECT_PROPERTY_STRING propertyconst string value,
      const int modifier = 0)
   {
      return ObjectSetString(chart(), name(), propertymodifiervalue);
   }
   
   virtual string name() = 0;
   virtual void name(const string) { }
   virtual long chart() { return 0; }
   virtual void chart(const long) { }
};

我们将在派生类中实现这些方法(后续我们将参照图表特性监视器的设计,扩展对象特性监视器的类层次结构)。

class ObjectSelectorpublic ObjectProxy
{
protected:
   long host// chart ID
   string id// chart ID
public:
   ObjectSelector(const string _idconst long _chart = 0): id(_id), host(_chart) { }
   
   virtual string name()
   {
      return id;
   }
   virtual void name(const string _id)
   {
      id = _id;
   }
   virtual void chart(const long _chartoverride
   {
      host = _chart;
   }
};

我们将抽象接口 ObjectProxy与其在ObjectSelector 中的最小实现分离,这是考虑到未来可能需要为同类型多对象实现代理数组。然后,只需在新建的 "multiselector" 类中存储名称数组或公共前缀,并通过调用重载的 [] 运算符确保从 name方法返回其中一个名称:multiSelector[i].get(OBJPROP_XYZ)

现在回到 ObjectCleanup2.mq5脚本,定义两个输入变量用于指定颜色和锚点,作为选择待删除对象的附加条件。

// ObjectCleanup2.mq5
...
input color CustomColor = clrRed;
input ENUM_ARROW_ANCHOR CustomAnchor = ANCHOR_TOP;

将这些值传递给 CustomDeleteAllObjects函数,借助中间类,对象循环中的新条件检查可以更精简地实现。

#include <MQL5Book/ObjectMonitor.mqh>
   
void OnStart()
{
   const int n = UseCustomDeleteAll ?
      CustomDeleteAllObjects(0ObjNamePrefixCustomColorCustomAnchor) :
      ObjectsDeleteAll(0ObjNamePrefix);
   PrintFormat("%d objects deleted"n);
}
   
int CustomDeleteAllObjects(const long chartconst string prefix,
   color clrENUM_ARROW_ANCHOR anchor,
   const int window = -1const int type = -1)
{
   int count = 0;
   for(int i = ObjectsTotal(chartwindowtype) - 1i >= 0; --i)
   {
      const string name = ObjectName(chartiwindowtype);
      
      ObjectSelector s(name);
      ResetLastError();
      if((StringLen(prefix) == 0 || StringFind(s.get(OBJPROP_NAME), prefix) == 0)
      && s.get(OBJPROP_COLOR) == CustomColor
      && s.get(OBJPROP_ANCHOR) == CustomAnchor
      && _LastError != 4203// OBJECT_WRONG_PROPERTY
      {
         count += ObjectDelete(chartname);
      }
   }
   return count;
}

需要重点说明的是,我们仅在创建ObjectSelector对象时指定一次对象名称(以及当前图表的隐式标识符 0)。后续所有特性均通过 get方法请求,该方法仅需一个描述所需特性的参数,编译器将自动选择对应的 ObjectGet 函数。

通过额外检查错误代码 4203 (OBJECT_WRONG_PROPERTY),可以过滤掉不具备所请求特性(例如 OBJPROP_ANCHOR)的对象。通过这种方式,尤其可以实现这样的筛选:所有类型的箭头均会被包含在内(无需分别请求不同类型的 OBJ_ARROW_XYZ),而线条和事件则会被排除在处理之外。

只需先在图表上运行 ObjectSimpleShowcase.mq5脚本(创建 14 种不同类型的对象),然后运行ObjectCleanup2.mq5 即可轻松验证这一点。如果启用UseCustomDeleteAll模式,图表上将保留 5 个未被删除的对象:OBJ_VLINE、OBJ_HLINE、OBJ_ARROW_BUY、OBJ_ARROW_SELL 和 OBJ_EVENT。前两个和最后一个对象不具备 OBJPROP_ANCHOR 特性,而买卖箭头对象则因颜色不符未被选中(默认其他所有创建的对象颜色均为红色)。

ObjectSelector的设计初衷并不仅限于上述简单应用。它是创建单个对象特性监视器的基础,类似于为图表实现的监视器。因此,ObjectMonitor.mqh头文件中包含更值得关注的内容。

class ObjectMonitorInterfacepublic ObjectSelector
{
public:
   ObjectMonitorInterface(const string _idconst long _chart = 0):
      ObjectSelector(_id_chart) { }
   virtual int snapshot() = 0;
   virtual void print() { };
   virtual int backup() { return 0; }
   virtual void restore() { }
   virtual void applyChanges(ObjectMonitorInterface *reference) { }
};

这组方法应该会让人联想到 ChartModeMonitor.mqh中的ChartModeMonitorInterface。唯一的创新是 applyChanges方法,该方法实现了将一个对象的特性复制到另一个对象的功能。

基于 ObjectMonitorInterface,以下是针对一对模板类型的特性监视器基本实现的描述:其中包含特性值类型(longdoublestring 三者之一)与枚举类型(ENUM_OBJECT_PROPERTY_ 风格的枚举之一)的组合。

template<typename T,typename E>
class ObjectMonitorBasepublic ObjectMonitorInterface
{
protected:
   MapArray<E,Tdata;  // array of pairs [property, value], current state
   MapArray<E,Tstore// backup (filled on demand)
   MapArray<E,Tchange;// committed changes between two states
   ...

ObjectMonitorBase构造函数包含两个参数:对象名称和标志数组,该数组包含需要在指定对象中观察的特性标识符。这段代码的大部分内容与ChartModeMonitor几乎完全相同。具体而言,与之前一样,标志数组会被传递给辅助方法 detect,该方法的主要目的是识别属于E 枚举元素的整数常量,并剔除所有其他内容。需要说明的唯一新增内容是通过ObjectGetInteger(0, id, OBJPROP_LEVELS)获取对象中级别数量的特性。这是为了支持因存在级别(例如 Fibonacci)而具有多个值的特性迭代所必需的。对于没有级别的对象,我们将获得数量 0,而此类特性将是普通的标量形式。

public:
   ObjectMonitorBase(const string _idconst int &flags[]): ObjectMonitorInterface(_id)
   {
      const int levels = (int)ObjectGetInteger(0idOBJPROP_LEVELS);
      for(int i = 0i < ArraySize(flags); ++i)
      {
         detect(flags[i], levels);
      }
   }
   ...

当然,detect方法与我们在ChartModeMonitor 中看到的有些不同。首先需要说明的是,它包含一个检查 v常量是否属于 E 枚举的代码片段,这是通过调用EnumToString 函数实现的:如果枚举中不存在该元素,将会引发错误代码。如果该元素存在,我们会将对应特性的值添加到 data数组中。

   // ChartModeMonitor.mqh
   bool detect(const int v)
   {
      ResetLastError();
      conststrings = EnumToString((E)v); // resulting string is not important
      if(_LastError == 0)                // analyze the error code
      {
         data.put((E)vget((E)v));
         return true;
      }
      return false;
   }

在对象监视器中,我们不得不将该方案复杂化,因为 ObjectGetObjectSet 函数中的 modifier 参数导致某些特性具有多值性。

因此我们引入了一个静态数组modifiables,其中列出了修饰符支持的特性(每个特性将在后续详细讨论)。关键在于,对于这类多值特性,需要多次读取并存储到 data数组中,而非仅一次。

// ObjectMonitor.mqh
   bool detect(const int vconst int levels)
   {
      // the following properties support multiple values
      static const int modifiables[] =
      {
         OBJPROP_TIME,        // anchor point by time
         OBJPROP_PRICE,       // anchor point by price
         OBJPROP_LEVELVALUE,  // level value
         OBJPROP_LEVELTEXT,   // inscription on the level line
         // NB: the following properties do not generate errors when exceeded
         // actual number of levels or files
         OBJPROP_LEVELCOLOR,  // level line color
         OBJPROP_LEVELSTYLE,  // level line style
         OBJPROP_LEVELWIDTH,  // width of the level line
         OBJPROP_BMPFILE,     // image files
      };
      ...

在这里,我们同样使用 EnumToString的技巧来检查是否存在具有v 标识符的特性。如果检查成功,我们会确认该特性是否在 modifiables列表中,并将相应的 modifiable 标志设为 truefalse

      bool result = false;
      ResetLastError();
 conststrings =EnumToString((E)v); // resulting string is not important
 if(_LastError ==0)// analyze the error code
      {
         bool modifiable = false;
         for(int i = 0i < ArraySize(modifiables); ++i)
         {
            if(v == modifiables[i])
            {
               modifiable = true;
               break;
            }
         }
         ...

默认情况下,任何特性都被视为无歧义,因此通过 ObjectGet函数读取或通过 ObjectSet 函数写入的所需次数为 1(即下方的 k 变量)。

         int k = 1;
         // for properties with modifiers, set the correct amount
         if(modifiable)
         {
            if(levels > 0k = levels;
            else if(v == OBJPROP_TIME || v == OBJPROP_PRICEk = MOD_MAX;
            else if(v == OBJPROP_BMPFILEk = 2;
         }

如果对象支持级别,我们会使用 levels参数限制潜在的读取/写入次数(如前所述,该参数在调用代码中通过 OBJPROP_LEVELS 特性获取)。

对于 OBJPROP_BMPFILE 特性(我们稍后会详细讨论),它只允许两种状态:开启(按钮按下,标志置位)或关闭(按钮释放,标志清零),因此 k = 2

最后,对象坐标特性 OBJPROP_TIME 和 OBJPROP_PRICE 很便捷,因为在尝试读取/写入不存在的锚点时会产生错误。因此,我们为 k分配一个明显较大的值 MOD_MAX,这样就能在 _LastError 非零值时中断读取点的循环。

         // read property value - one or many
         for(int i = 0i < k; ++i)
         {
            ResetLastError();
            T temp = get((E)vi);
            // if there is no i-th modifier, we will get an error and break the loop
            if(_LastError != 0break;
            data.put((E)MOD_COMBINE(vi), temp);
            result = true;
         }
      }
      return result;
   }

由于一个特性可能有多个值(通过循环读取直至 k次),我们不能再简单地编写data.put((E)v, get((E)v))。我们需要以某种方式组合特性标识符 v及其修改编号i。所幸的是,特性数量也受限于整数常量 (typeint),最多占用两个低位字节。因此我们可以使用位操作符将i存入高字节。为此专门开发了 MOD_COMBINE 宏。

#define MOD_COMBINE(V,I) (V | (I << 24))

当然,也提供了反向宏来提取特性 ID 和修订编号。

#define MOD_GET_NAME(V)  (V & 0xFFFFFF)
#define MOD_GET_INDEX(V) (V >> 24)

例如,在 snapshot方法中可以看到它们的使用方式。

   virtual int snapshot() override
   {
      MapArray<E,Ttemp;
      change.reset();
      
      // collect all required properties in temp
      for(int i = 0i < data.getSize(); ++i)
      {
         const E e = (E)MOD_GET_NAME(data.getKey(i));
         const int m = MOD_GET_INDEX(data.getKey(i));
         temp.put((E)data.getKey(i), get(em));
      }
      
      int changes = 0;
      // compare previous and new state
      for(int i = 0i < data.getSize(); ++i)
      {
         if(data[i] != temp[i])
         {
            // save the differences in the change array
            if(changes == 0Print(id);
            const E e = (E)MOD_GET_NAME(data.getKey(i));
            const int m = MOD_GET_INDEX(data.getKey(i));
            Print(EnumToString(e), (m > 0 ? (string)m : ""), " "data[i], " -> "temp[i]);
            change.put(data.getKey(i), temp[i]);
            changes++;
         }
      }
      
      // save the new state as current
      data = temp;
      return changes;
   }

该方法复用了 ChartModeMonitor.mqh中同名方法的全部逻辑,但在任何位置读取特性时,必须首先通过 MOD_GET_NAME 从存储的键中提取特性名称,并通过 MOD_GET_INDEX 提取编号。

restore方法也不得不进行类似的复杂操作。

   virtual void restore() override
   {
      data = store;
      for(int i = 0i < data.getSize(); ++i)
      {
         const E e = (E)MOD_GET_NAME(data.getKey(i));
         const int m = MOD_GET_INDEX(data.getKey(i));
         set(edata[i], m);
      }
   }

ObjectMonitorBase最具创新性的特性在于其如何处理变更。

   MapArray<E,T> * const getChanges()
   {
      return &change;
   }
   
   virtual void applyChanges(ObjectMonitorInterface *intfoverride
   {
      ObjectMonitorBase *reference = dynamic_cast<ObjectMonitorBase<T,E> *>(intf);
      if(reference)
      {
         MapArray<E,T> *event = reference.getChanges();
         if(event.getSize() > 0)
         {
            Print("Modifing "id" by "event.getSize(), " changes");
            for(int i = 0i < event.getSize(); ++i)
            {
               data.put(event.getKey(i), event[i]);
               const E e = (E)MOD_GET_NAME(event.getKey(i));
               const int m = MOD_GET_INDEX(event.getKey(i));
               Print(EnumToString(e), " "m" "event[i]);
               set(eevent[i], m);
            }
         }
      }
   }

通过向 applyChanges方法传递另一个对象的监控器状态,我们可以采用该对象的所有最新变更。

为支持所有三种基本类型(longdoublestring)的特性,我们需要实现 ObjectMonitor 类(相当于 ChartModeMonitor.mqh 中的 ChartModeMonitor)。

class ObjectMonitorpublic ObjectMonitorInterface
{
protected:
   AutoPtr<ObjectMonitorInterfacem[3];
   
   ObjectMonitorInterface *getBase(const int i)
   {
      return m[i][];
   }
   
public:
   ObjectMonitor(const string objidconst int &flags[]): ObjectMonitorInterface(objid)
   {
      m[0] = new ObjectMonitorBase<long,ENUM_OBJECT_PROPERTY_INTEGER>(objidflags);
      m[1] = new ObjectMonitorBase<double,ENUM_OBJECT_PROPERTY_DOUBLE>(objidflags);
      m[2] = new ObjectMonitorBase<string,ENUM_OBJECT_PROPERTY_STRING>(objidflags);
   }
   ...

此处保留了之前的代码结构,仅新增了支持变更和名称的方法(我们应该还记得,图表本身不具备名称)。

   ...
   virtual string name() override
   {
      return m[0][].name();
   }
   
   virtual void name(const string objidoverride
   {
      m[0][].name(objid);
      m[1][].name(objid);
      m[2][].name(objid);
   }
   
   virtual void applyChanges(ObjectMonitorInterface *intfoverride
   {
      ObjectMonitor *monitor = dynamic_cast<ObjectMonitor *>(intf);
      if(monitor)
      {
         m[0][].applyChanges(monitor.getBase(0));
         m[1][].applyChanges(monitor.getBase(1));
         m[2][].applyChanges(monitor.getBase(2));
      }
   }

基于已创建的对象监视器,可以轻松实现终端中不支持的多项实用功能。特别是对象副本创建和对象批量编辑功能。

ObjectCopy 脚本

ObjectCopy.mq5脚本演示了如何复制选中的对象。在其 OnStart函数起始阶段,我们用连续整数填充flags 数组,这些整数是各类 ENUM_OBJECT_PROPERTY_ 枚举元素的候选值。枚举元素的编号按功能用途明显分组,且组间存在较大间隔(显然是为未来元素预留空间),因此形成的数组相当大:2048 个元素。

#include <MQL5Book/ObjectMonitor.mqh>
   
#define PUSH(A,V) (A[ArrayResize(AArraySize(A) + 1) - 1] = V)
   
void OnStart()
{
   int flags[2048];
   // filling the array with consecutive integers, which will be
   // checked against the elements of enumerations of object properties,
   // invalid values will be discarded in the monitor's detect method
   for(int i = 0i < ArraySize(flags); ++i)
   {
      flags[i] = i;
   }
   ...

接下来,我们将图表当前选中的对象名称收集到数组中。为此,我们使用 OBJPROP_SELECTED 特性。

   string selected[];
   const int n = ObjectsTotal(0);
   for(int i = 0i < n; ++i)
   {
      const string name = ObjectName(0i);
      if(ObjectGetInteger(0nameOBJPROP_SELECTED))
      {
         PUSH(selectedname);
      }
   }
   ...

最终,在遍历选中元素的主循环中,我们依次读取每个对象的特性、生成其副本名称并创建具有相同特性集合的新对象。

   for(int i = 0i < ArraySize(selected); ++i)
   {
      const string name = selected[i];
      
     // make a backup of the properties of the current object using the monitor
      ObjectMonitor object(nameflags);
      object.print();
      object.backup();
      // form a correct, appropriate name for the copy
      const string copy = GetFreeName(name);
      
      if(StringLen(copy) > 0)
      {
         Print("Copy name: "copy);
         // create an object of the same type OBJPROP_TYPE
         ObjectCreate(0copy,
            (ENUM_OBJECT)ObjectGetInteger(0nameOBJPROP_TYPE),
            ObjectFind(0name), 00);
         // change the name of the object in the monitor to a new one
         object.name(copy);
         // restore all properties from the backup to a new object
         object.restore();
      }
      else
      {
         Print("Can't create copy name for: "name);
      }
   }
}

这里需要注意,OBJPROP_TYPE 特性是少数只读特性之一,因此必须首先创建所需类型的对象。

辅助函数GetFreeName尝试在对象名后追加 "/Copy #x" 字符串,其中 x 为副本编号。因此,通过多次运行脚本,可创建第 2 个、第 3 个等副本。

string GetFreeName(const string name)
{
   const string suffix = "/Copy №";
   // check if there is a copy in the suffix name
   const int pos = StringFind(namesuffix);
   string prefix;
   int n;
   
   if(pos <= 0)
   {
      // if suffix is not found, assume copy number 1
      const string candidate = name + suffix + "1";
      // checking if the copy name is free, and if so, return it
      if(ObjectFind(0candidate) < 0)
      {
         return candidate;
      }
      // otherwise, prepare for a loop with iteration of copy numbers
      prefix = name;
      n = 0;
   }
   else
   {
      // if the suffix is found, select the name without it
      prefix = StringSubstr(name0pos);
      // and find the copy number in the string
      n = (int)StringToInteger(StringSubstr(namepos + StringLen(suffix)));
   }
   
   Print("Found: "prefix" "n);
   // loop trying to find a free copy number above n, but no more than 1000
   for(int i = n + 1i < 1000; ++i)
   {
      const string candidate = prefix + suffix + (string)i;
      // check for the existence of an object with a name ending "Copy #i"
      if(ObjectFind(0candidate) < 0)
      {
         return candidate// return vacant copy name
      }
   }
   return NULL// too many copies
}

终端会记住特定类型对象的最后设置,如果连续创建多个同类对象,其效果等同于复制。然而在使用不同图表的过程中,这些设置通常会发生变化,如果一段时间后需要复制某些“旧”对象,通常需要完全重新配置其设置。这对于特性数量庞大的对象类型(例如 Fibonacci 工具)尤其耗时。在这种情况下,该脚本将发挥重要作用。

本章中某些包含相同类型对象的图片,正是使用该脚本创建的。

ObjectGroupEdit indicator

ObjectMonitor的第二个应用示例是 ObjectGroupEdit.mq5 指标,该指标支持一次性编辑一组选定对象的特性。

假设我们在图表上选中了多个对象(不一定是相同类型),需要统一修改其某个特性。接下来,打开任意选中对象的特性对话框进行配置,点击OK后,这些更改将应用于所有选中对象。这就是我们下一个 MQL 程序的运行方式。

我们需要将指标作为程序类型,因为它涉及图表事件。关于 MQL5 编程的这一方面,后续将有整个专门的 章节详述,但我们现在就需要掌握部分基础知识。

由于指标没有图表,#property指令设为零,且 OnCalculate 函数几乎为空。

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0
   
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,
                const double &price[])
{
   return rates_total;
}

为了自动生成对象的完整特性集,我们将再次使用包含 2048 个连续整数值的数组。我们还将提供一个用于存储所选元素名称的数组,以及一个由 ObjectMonitor类组成的监控对象数组。

int consts[2048];
string selected[];
ObjectMonitor *objects[];

OnInit处理程序中,我们将初始化数字数组并启动计时器。

void OnInit()
{
   for(int i = 0i < ArraySize(consts); ++i)
   {
      consts[i] = i;
   }
   
   EventSetTimer(1);
}

在计时器处理程序中,我们将所选对象的名称保存到数组中。如果选择列表发生变化,则需要重新配置监控对象,此时会调用辅助函数 TrackSelectedObjects

void OnTimer()
{
   string updates[];
   const int n = ObjectsTotal(0);
   for(int i = 0i < n; ++i)
   {
      const string name = ObjectName(0i);
      if(ObjectGetInteger(0nameOBJPROP_SELECTED))
      {
         PUSH(updatesname);
      }
   }
   
   if(ArraySize(selected) != ArraySize(updates))
   {
      ArraySwap(selectedupdates);
      Comment("Selected objects: "ArraySize(selected));
      TrackSelectedObjects();
   }
}

TrackSelectedObjects函数本身非常简单:删除旧的监控对象并创建新的监控对象。如果你需要,可以通过保留选择中未变动的部分,使其更加智能化。

void TrackSelectedObjects()
{
   for(int j = 0j < ArraySize(objects); ++j)
   {
      delete objects[j];
   }
   
   ArrayResize(objects0);
   
   for(int i = 0i < ArraySize(selected); ++i)
   {
      const string name = selected[i];
      PUSH(objectsnew ObjectMonitor(nameconsts));
   }
}

请注意,当创建监控对象时,它会立即捕获对应图形对象的所有特性。

现在我们终于要讲到事件处理的部分。正如在 事件函数概述中已经提到的,该处理程序负责处理图表上的OnChartEvent事件。在本示例中,我们重点关注特定的 CHARTEVENT_OBJECT_CHANGE 事件:当用户在对象特性对话框中更改任何特性时,该事件会被触发。被修改对象的名称通过 sparam参数传递。

如果该名称与某个被监控对象匹配,我们会要求监控器对其特性生成新的快照,即调用 objects[i].snapshot()

void OnChartEvent(const int id,
   const long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_OBJECT_CHANGE)
   {
      Print("Object changed: "sparam);
      for(int i = 0i < ArraySize(selected); ++i)
      {
         if(sparam == selected[i])
         {
            const int changes = objects[i].snapshot();
            if(changes > 0)
            {
               for(int j = 0j < ArraySize(objects); ++j)
               {
                  if(j != i)
                  {
                     objects[j].applyChanges(objects[i]);
                  }
               }
            }
            ChartRedraw();
            break;
         }
      }
   }
}

如果确认存在变更(一般都会有变更),则changes变量中的数量将大于 0。然后,开始遍历所有选中的对象,并将检测到的变更应用到每个对象,但原始对象除外。

由于我们可能需要修改多个对象,因此需要通过 ChartRedraw请求图表重绘。

OnDeinit处理程序中,我们将移除所有监控器。

void OnDeinit(const int)
{
   for(int j = 0j < ArraySize(objects); ++j)
   {
      delete objects[j];
   }
   Comment("");
}

至此,新工具已准备就绪。

该指标允许你针对 定义对象锚点章节中多组标签对象的通用外观进行自定义。

值得一提的是,基于 ObjectMonitor的类似原理,还可以创建终端中尚未提供的另一个常用工具:撤销对对象特性的编辑操作,因为 restore 方法现在可供使用。