对象特性访问函数概述
对象具有多种类型的特性,这些特性可通过 ObjectGet和ObjectSet 函数进行读取和设置。我们知道,这一原则已在图表中应用(请参阅 用于处理完整图表特性集的函数概述 章节)。
所有此类函数的前三个参数均为图表标识符、对象名称和特性标识符,其中特性标识符必须是 ENUM_OBJECT_PROPERTY_INTEGER、ENUM_OBJECT_PROPERTY_DOUBLE 或 ENUM_OBJECT_PROPERTY_STRING 枚举的成员。我们将在后续章节逐步研究具体特性。完整的特性对照表可在 MQL5 文档的 对象特性页面上找到。
需要注意的是,所有三个枚举中的特性标识符互不重叠,这使得我们可以将它们的联合处理合并为单个统一代码。我们将在示例中运用这一点。
某些特性为只读,并将标记为 "r/o"(只读)。
与绘图 API 的情况类似,特性读取函数具有简写和完整两种形式:简写形式直接返回请求的值,完整形式返回布尔值(true表示成功,false 表示发生错误),实际值则通过引用传递的最后一个参数返回。调用简写形式时,必须通过内置变量 _LastError检查是否发生错误。
访问某些特性时,必须指定一个额外参数 (modifier),用于在特性为多值时指示值的编号或级别。例如,如果一个对象有多个锚点,则修饰符可用于选择特定的锚点。
以下是用于读取和写入整数型特性的函数原型。需特别注意,这些函数的值类型为 long,因此不仅允许存储 int 或 long 类型的特性,还可以存储 bool、color 或 datetime 类型的特性以及各种枚举(见下文)。
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)
为提升性能,所有设置对象特性的函数(ObjectSetInteger、ObjectSetDouble 和ObjectSetString)均为异步函数,本质上是向图表发送修改对象的命令。当这些函数成功执行时,命令将被存入图表的共享事件队列,并返回结果true表示操作成功。如果发生错误,函数将返回 false,,此时必须通过 _LastError 变量检查错误代码。
对象特性的更改存在一定延迟,在图表事件队列处理期间完成。要强制更新图表上对象的外观和特性(尤其是在一次性更改多个对象后),可使用 ChartRedraw 函数。
获取图表特性的函数(ObjectGetInteger、ObjectGetDouble 和ObjectGetString)是同步的,这意味着调用代码会等待这些函数的执行结果。在这种情况下,图表队列中的所有命令均被执行,以获取最新特性值。
让我们回到 删除对象脚本的示例,更准确地说,是其新版本ObjectCleanup2.mq5。回顾在CustomDeleteAllObjects函数中,我们希望实现根据对象特性选择对象的功能。假设这些特性应该是颜色和锚点。要获取这些特性,可使用ObjectGetInteger函数以及 ENUM_OBJECT_PROPERTY_INTEGER 枚举中的一对元素:OBJPROP_COLOR 和 OBJPROP_ANCHOR。我们将在后续详细探讨这些内容。
基于此,代码需补充以下检查逻辑(为简化演示,此处颜色和锚点分别使用clrRed和 ANCHOR_TOP 常量表示,实际应用中我们会提供对应的输入变量)。
int CustomDeleteAllObjects(const long chart, const string prefix,
const int window = -1, const int type = -1)
{
int count = 0;
for(int i = ObjectsTotal(chart, window, type) - 1; i >= 0; --i)
{
const string name = ObjectName(chart, i, window, type);
// condition on the name and additional properties, such as color and anchor point
if((StringLen(prefix) == 0 || StringFind(name, prefix) == 0)
&& ObjectGetInteger(0, name, OBJPROP_COLOR) == clrRed
&& ObjectGetInteger(0, name, OBJPROP_ANCHOR) == ANCHOR_TOP)
{
count += ObjectDelete(chart, name);
}
}
return count;
}
|
请注意包含 ObjectGetInteger的代码行。
这些代码较长且存在重复,因为特定特性与已知类型的ObjectGet函数绑定。此外,随着条件数量的增加,重复填写图表 ID 和对象名称会显得冗余。
为简化代码结构,我们将采用在 图表显示模式章节中ChartModeMonitor.mqh文件中测试过的技术。这样做的意义在于构建一个中间类,通过方法重载实现对所有类型特性的读写操作。我们将新的头文件命名为 ObjectMonitor.mqh。
ObjectProxy类的结构与用于图表ChartModeMonitorInterface 类高度相似。主要区别在于它包含用于设置和获取图表 ID 及对象名称的虚方法。
class ObjectProxy
{
public:
long get(const ENUM_OBJECT_PROPERTY_INTEGER property, const int modifier = 0)
{
return ObjectGetInteger(chart(), name(), property, modifier);
}
double get(const ENUM_OBJECT_PROPERTY_DOUBLE property, const int modifier = 0)
{
return ObjectGetDouble(chart(), name(), property, modifier);
}
string get(const ENUM_OBJECT_PROPERTY_STRING property, const int modifier = 0)
{
return ObjectGetString(chart(), name(), property, modifier);
}
bool set(const ENUM_OBJECT_PROPERTY_INTEGER property, const long value,
const int modifier = 0)
{
return ObjectSetInteger(chart(), name(), property, modifier, value);
}
bool set(const ENUM_OBJECT_PROPERTY_DOUBLE property, const double value,
const int modifier = 0)
{
return ObjectSetDouble(chart(), name(), property, modifier, value);
}
bool set(const ENUM_OBJECT_PROPERTY_STRING property, const string value,
const int modifier = 0)
{
return ObjectSetString(chart(), name(), property, modifier, value);
}
virtual string name() = 0;
virtual void name(const string) { }
virtual long chart() { return 0; }
virtual void chart(const long) { }
};
|
我们将在派生类中实现这些方法(后续我们将参照图表特性监视器的设计,扩展对象特性监视器的类层次结构)。
class ObjectSelector: public ObjectProxy
{
protected:
long host; // chart ID
string id; // chart ID
public:
ObjectSelector(const string _id, const 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 _chart) override
{
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(0, ObjNamePrefix, CustomColor, CustomAnchor) :
ObjectsDeleteAll(0, ObjNamePrefix);
PrintFormat("%d objects deleted", n);
}
int CustomDeleteAllObjects(const long chart, const string prefix,
color clr, ENUM_ARROW_ANCHOR anchor,
const int window = -1, const int type = -1)
{
int count = 0;
for(int i = ObjectsTotal(chart, window, type) - 1; i >= 0; --i)
{
const string name = ObjectName(chart, i, window, type);
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(chart, name);
}
}
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 ObjectMonitorInterface: public ObjectSelector
{
public:
ObjectMonitorInterface(const string _id, const 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,以下是针对一对模板类型的特性监视器基本实现的描述:其中包含特性值类型(long、double 或 string 三者之一)与枚举类型(ENUM_OBJECT_PROPERTY_ 风格的枚举之一)的组合。
template<typename T,typename E>
class ObjectMonitorBase: public ObjectMonitorInterface
{
protected:
MapArray<E,T> data; // array of pairs [property, value], current state
MapArray<E,T> store; // backup (filled on demand)
MapArray<E,T> change;// committed changes between two states
...
|
ObjectMonitorBase构造函数包含两个参数:对象名称和标志数组,该数组包含需要在指定对象中观察的特性标识符。这段代码的大部分内容与ChartModeMonitor几乎完全相同。具体而言,与之前一样,标志数组会被传递给辅助方法 detect,该方法的主要目的是识别属于E 枚举元素的整数常量,并剔除所有其他内容。需要说明的唯一新增内容是通过ObjectGetInteger(0, id, OBJPROP_LEVELS)获取对象中级别数量的特性。这是为了支持因存在级别(例如 Fibonacci)而具有多个值的特性迭代所必需的。对于没有级别的对象,我们将获得数量 0,而此类特性将是普通的标量形式。
public:
ObjectMonitorBase(const string _id, const int &flags[]): ObjectMonitorInterface(_id)
{
const int levels = (int)ObjectGetInteger(0, id, OBJPROP_LEVELS);
for(int i = 0; i < 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)v, get((E)v));
return true;
}
return false;
}
|
在对象监视器中,我们不得不将该方案复杂化,因为 ObjectGet和 ObjectSet 函数中的 modifier 参数导致某些特性具有多值性。
因此我们引入了一个静态数组modifiables,其中列出了修饰符支持的特性(每个特性将在后续详细讨论)。关键在于,对于这类多值特性,需要多次读取并存储到 data数组中,而非仅一次。
// ObjectMonitor.mqh
bool detect(const int v, const 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 标志设为 true 或 false。
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 = 0; i < 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 > 0) k = levels;
else if(v == OBJPROP_TIME || v == OBJPROP_PRICE) k = MOD_MAX;
else if(v == OBJPROP_BMPFILE) k = 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 = 0; i < k; ++i)
{
ResetLastError();
T temp = get((E)v, i);
// if there is no i-th modifier, we will get an error and break the loop
if(_LastError != 0) break;
data.put((E)MOD_COMBINE(v, i), 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,T> temp;
change.reset();
// collect all required properties in temp
for(int i = 0; i < 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(e, m));
}
int changes = 0;
// compare previous and new state
for(int i = 0; i < data.getSize(); ++i)
{
if(data[i] != temp[i])
{
// save the differences in the change array
if(changes == 0) Print(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 = 0; i < data.getSize(); ++i)
{
const E e = (E)MOD_GET_NAME(data.getKey(i));
const int m = MOD_GET_INDEX(data.getKey(i));
set(e, data[i], m);
}
}
|
ObjectMonitorBase最具创新性的特性在于其如何处理变更。
MapArray<E,T> * const getChanges()
{
return &change;
}
virtual void applyChanges(ObjectMonitorInterface *intf) override
{
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 = 0; i < 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(e, event[i], m);
}
}
}
}
|
通过向 applyChanges方法传递另一个对象的监控器状态,我们可以采用该对象的所有最新变更。
为支持所有三种基本类型(long、double、string)的特性,我们需要实现 ObjectMonitor 类(相当于 ChartModeMonitor.mqh 中的 ChartModeMonitor)。
class ObjectMonitor: public ObjectMonitorInterface
{
protected:
AutoPtr<ObjectMonitorInterface> m[3];
ObjectMonitorInterface *getBase(const int i)
{
return m[i][];
}
public:
ObjectMonitor(const string objid, const int &flags[]): ObjectMonitorInterface(objid)
{
m[0] = new ObjectMonitorBase<long,ENUM_OBJECT_PROPERTY_INTEGER>(objid, flags);
m[1] = new ObjectMonitorBase<double,ENUM_OBJECT_PROPERTY_DOUBLE>(objid, flags);
m[2] = new ObjectMonitorBase<string,ENUM_OBJECT_PROPERTY_STRING>(objid, flags);
}
...
|
此处保留了之前的代码结构,仅新增了支持变更和名称的方法(我们应该还记得,图表本身不具备名称)。
...
virtual string name() override
{
return m[0][].name();
}
virtual void name(const string objid) override
{
m[0][].name(objid);
m[1][].name(objid);
m[2][].name(objid);
}
virtual void applyChanges(ObjectMonitorInterface *intf) override
{
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(A, ArraySize(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 = 0; i < ArraySize(flags); ++i)
{
flags[i] = i;
}
...
|
接下来,我们将图表当前选中的对象名称收集到数组中。为此,我们使用 OBJPROP_SELECTED 特性。
string selected[];
const int n = ObjectsTotal(0);
for(int i = 0; i < n; ++i)
{
const string name = ObjectName(0, i);
if(ObjectGetInteger(0, name, OBJPROP_SELECTED))
{
PUSH(selected, name);
}
}
...
|
最终,在遍历选中元素的主循环中,我们依次读取每个对象的特性、生成其副本名称并创建具有相同特性集合的新对象。
for(int i = 0; i < ArraySize(selected); ++i)
{
const string name = selected[i];
// make a backup of the properties of the current object using the monitor
ObjectMonitor object(name, flags);
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(0, copy,
(ENUM_OBJECT)ObjectGetInteger(0, name, OBJPROP_TYPE),
ObjectFind(0, name), 0, 0);
// 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(name, suffix);
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(0, candidate) < 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(name, 0, pos);
// and find the copy number in the string
n = (int)StringToInteger(StringSubstr(name, pos + 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 + 1; i < 1000; ++i)
{
const string candidate = prefix + suffix + (string)i;
// check for the existence of an object with a name ending "Copy #i"
if(ObjectFind(0, candidate) < 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 = 0; i < ArraySize(consts); ++i)
{
consts[i] = i;
}
EventSetTimer(1);
}
|
在计时器处理程序中,我们将所选对象的名称保存到数组中。如果选择列表发生变化,则需要重新配置监控对象,此时会调用辅助函数 TrackSelectedObjects。
void OnTimer()
{
string updates[];
const int n = ObjectsTotal(0);
for(int i = 0; i < n; ++i)
{
const string name = ObjectName(0, i);
if(ObjectGetInteger(0, name, OBJPROP_SELECTED))
{
PUSH(updates, name);
}
}
if(ArraySize(selected) != ArraySize(updates))
{
ArraySwap(selected, updates);
Comment("Selected objects: ", ArraySize(selected));
TrackSelectedObjects();
}
}
|
TrackSelectedObjects函数本身非常简单:删除旧的监控对象并创建新的监控对象。如果你需要,可以通过保留选择中未变动的部分,使其更加智能化。
void TrackSelectedObjects()
{
for(int j = 0; j < ArraySize(objects); ++j)
{
delete objects[j];
}
ArrayResize(objects, 0);
for(int i = 0; i < ArraySize(selected); ++i)
{
const string name = selected[i];
PUSH(objects, new ObjectMonitor(name, consts));
}
}
|
请注意,当创建监控对象时,它会立即捕获对应图形对象的所有特性。
现在我们终于要讲到事件处理的部分。正如在 事件函数概述中已经提到的,该处理程序负责处理图表上的OnChartEvent事件。在本示例中,我们重点关注特定的 CHARTEVENT_OBJECT_CHANGE 事件:当用户在对象特性对话框中更改任何特性时,该事件会被触发。被修改对象的名称通过 sparam参数传递。
如果该名称与某个被监控对象匹配,我们会要求监控器对其特性生成新的快照,即调用 objects[i].snapshot()。
void OnChartEvent(const int id,
const long &lparam, const double &dparam, const string &sparam)
{
if(id == CHARTEVENT_OBJECT_CHANGE)
{
Print("Object changed: ", sparam);
for(int i = 0; i < ArraySize(selected); ++i)
{
if(sparam == selected[i])
{
const int changes = objects[i].snapshot();
if(changes > 0)
{
for(int j = 0; j < ArraySize(objects); ++j)
{
if(j != i)
{
objects[j].applyChanges(objects[i]);
}
}
}
ChartRedraw();
break;
}
}
}
}
|
如果确认存在变更(一般都会有变更),则changes变量中的数量将大于 0。然后,开始遍历所有选中的对象,并将检测到的变更应用到每个对象,但原始对象除外。
由于我们可能需要修改多个对象,因此需要通过 ChartRedraw请求图表重绘。
在 OnDeinit处理程序中,我们将移除所有监控器。
void OnDeinit(const int)
{
for(int j = 0; j < ArraySize(objects); ++j)
{
delete objects[j];
}
Comment("");
}
|
至此,新工具已准备就绪。
该指标允许你针对 定义对象锚点章节中多组标签对象的通用外观进行自定义。
值得一提的是,基于 ObjectMonitor的类似原理,还可以创建终端中尚未提供的另一个常用工具:撤销对对象特性的编辑操作,因为 restore 方法现在可供使用。