Обзор функций доступа к свойствам объектов по типам значений

Объекты обладают свойствами различных типов, которые можно читать и устанавливаться с помощью нескольких ObjectGet- и ObjectSet-функций. Как мы знаем, данный принцип уже применялся для графиков (см. раздел Обзор функций для работы с полным набором свойств графиков).

Все такие функции принимают в качестве первых трех параметров идентификатор графика, название объекта и идентификатор свойства, который должен быть элементом одного из перечислений ENUM_OBJECT_PROPERTY_INTEGER, ENUM_OBJECT_PROPERTY_DOUBLE или ENUM_OBJECT_PROPERTY_STRING. Конкретные свойства мы изучим постепенно в следующих разделах. Их полные сводные таблицы можно найти в документации по MQL5, на странице Свойства объектов.

Следует отметить, что идентификаторы свойств во всех трех перечислениях не пересекаются, что позволяет объединять в единый унифицированный код их совместную обработку. Мы воспользуется этим в примерах.

Некоторые свойства доступны только на чтение и будут помечаться "r/o" (read-only).

Также как и в случае 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 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);
      // условие на имя и дополнительные свойства, например, цвет и точку привязки
      if((StringLen(prefix) == 0 || StringFind(nameprefix) == 0)
         && ObjectGetInteger(0nameOBJPROP_COLOR) == clrRed
         && ObjectGetInteger(0nameOBJPROP_ANCHOR) == ANCHOR_TOP)
      {
         count += ObjectDelete(chartname);
      }
   }
   return count;
}

Обратим внимание на строки с ObjectGetInteger.

Их запись длинна и содержит некоторую тавтологию, потому что конкретные свойства привязаны к ObjectGet-функциям известных типов. Кроме того, по мере увеличения количества условий могут показаться избыточными повторения идентификатора графика и имени объекта.

Чтобы упростить запись обратимся к технологии, которую мы опробовали в файле ChartModeMonitor.mqh в разделе Режимы отображения графика. Её суть в том, чтобы описать класс-посредник с перегрузками методов для чтения и записи свойств всех типов. Назовем новый заголовочный файл ObjectMonitor.mqh.

Класс ObjectProxy почти полностью повторяет для объектов структуру класса ChartModeMonitorInterface для графиков. Основное отличие заключается в наличии виртуальных методов для установки и получения идентификатора графика и имени объекта.

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// идентификатор графика
   string 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, потому что позднее вам может потребоваться реализовать, например, массив посредников для множества однотипных объектов. Тогда достаточно в новом классе "мультиселектора" хранить массив имен или их общий префикс и обеспечивать возврат одного из них из метода 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;
}

Важно отметить, что имя объекта (и неявный идентификатор текущего графика 0) мы указываем лишь раз — при создании объекта ObjectSelector. Далее все свойства запрашиваются методом 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) { }
};

Данный набор методов должен напомнить вам ChartModeMonitorInterface из ChartModeMonitor.mqh. Единственное новшество — это метод applyChanges: он предназначен для копирования свойств одного объекта в другой.

Отталкиваясь от ObjectMonitorInterface описана базовая реализация монитора свойств для пары шаблонных типов: типа значений свойств (одного из long, double, string) и типа перечислений (одного из ENUM_OBJECT_PROPERTY_-что-то там).

template<typename T,typename E>
class ObjectMonitorBasepublic ObjectMonitorInterface
{
protected:
   MapArray<E,Tdata;  // массив пар [свойство,значение], текущее состояние
   MapArray<E,Tstore// бэкап (заполняется по требованию)
   MapArray<E,Tchange;// зафиксированные изменения между двумя состояниями
   ...

Конструктор ObjectMonitorBase имеет два параметра: имя объекта и массив флагов с идентификаторами свойств, подлежащих наблюдению в указанном объекте. Большая часть этого кода почти 1 в 1 повторяет ChartModeMonitor. В частности, как и ранее, массив флагов передается во вспомогательный метод detect, основное назначение которого выявить те целочисленные константы, которые являются элементами перечисления E, и отсеять все остальные. Единственное дополнение, которое следует пояснить, — получение свойства с количеством уровней в объекте посредством ObjectGetInteger(0, id, OBJPROP_LEVELS). Это нужно, чтобы поддержать перебор свойств со множественными значениями из-за наличия уровней (например, Фибоначчи). У объектов без уровней мы получим количество 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();
      const string s = EnumToString((E)v); // результирующая строка не важна
      if(_LastError == 0)                  // анализируем код ошибки
      {
         data.put((E)vget((E)v));
         return true;
      }
      return false;
   }

В мониторе объектов мы вынуждены усложнить данную схему, поскольку некоторые свойства являются многозначными из-за параметра modifier в ObjectGet- и ObjectSet-функциях.

Поэтому мы вводим статический массив modifiables с перечнем тех свойств, которые поддерживают модификаторы (каждое свойство будет подробно рассмотрено в дальнейшем). Суть в том, что для подобных многозначных свойств необходимо их считывать и сохранять в массив data не единожды, а несколько раз.

// ObjectMonitor.mqh
   bool detect(const int vconst int levels)
   {
      // следующие свойства поддерживают множественность значений
      static const int modifiables[] =
      {
         OBJPROP_TIME,        // точка привязки по времени
         OBJPROP_PRICE,       // точка привязки по цене
         OBJPROP_LEVELVALUE,  // значение уровня
         OBJPROP_LEVELTEXT,   // надпись на линии уровня
         // NB: следующие свойства не генерируют ошибки при превышении
         // фактического количества уровней или файлов
         OBJPROP_LEVELCOLOR,  // цвет линии уровня
         OBJPROP_LEVELSTYLE,  // стиль линии уровня
         OBJPROP_LEVELWIDTH,  // толщина линии уровня
         OBJPROP_BMPFILE,     // файлы с изображениями
      };
      ...

Здесь, разумеется, тоже используется трюк с EnumToString, чтобы проверить существование свойства с идентификатором v. В случае успеха мы проверяем его наличие в списке modifiables, и устанавливаем соответствующий флаг modifiable в true или false.

      bool result = false;
      ResetLastError();
      const string s = EnumToString((E)v); // результирующая строка не важна
      if(_LastError == 0)                  // анализируем код ошибки
      {
         bool modifiable = false;
         for(int i = 0i < ArraySize(modifiables); ++i)
         {
            if(v == modifiables[i])
            {
               modifiable = true;
               break;
            }
         }
         ...

По умолчанию любое свойство считается однозначным и потому необходимое количество чтений через ObjectGet-функцию или записей через ObjectSet-функцию равно 1 (переменная k ниже).

         int k = 1;
         // для свойств с модификаторами ставим правильное количество
         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.

         // читаем значение свойства - одно или много
         for(int i = 0i < k; ++i)
         {
            ResetLastError();
            T temp = get((E)vi);
            // если i-го модификатора нет, получим ошибку и прервем цикл 
            if(_LastError != 0break;
            data.put((E)MOD_COMBINE(vi), temp);
            result = true;
         }
      }
      return result;
   }

Поскольку у одного свойства может быть несколько значений, которые читаются в цикле вплоть до k, мы уже не можем просто написать data.put((E)v, get((E)v)). Нам необходимо неким образом совместить идентификатор свойства v и номер его модификации i. К счастью, количество свойств ограничено и в целочисленной константе (тип int) занято не более двух младших байтов. Поэтому мы можем с помощью побитовых операторов поместить i в верхний байт. Для этой цели разработан макрос MOD_COMBINE.

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

Разумеется, для извлечения идентификатора свойства и номера модификации предусмотрены обратные макросы.

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

Вот, например, как они используются в методе snapshot.

   virtual int snapshot() override
   {
      MapArray<E,Ttemp;
      change.reset();
      
      // собираем все требуемые свойства в 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;
      // сравниваем предыдущее и новое состояние
      for(int i = 0i < data.getSize(); ++i)
      {
         if(data[i] != temp[i])
         {
            // сохраняем различия в массив change
            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++;
         }
      }
      
      // сохраняем новое состояние как текущее
      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 монитор состояния другого объекта, мы можем перенять из него все последние изменения.

Для поддержки свойств всех трех основных типов (long, double, string) нам требуется реализовать класс ObjectMonitor (аналог ChartModeMonitor из ChartModeMonitor.mqh).

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];
   // заполняем массив последовательными целыми числами, которые будут
   // проверены на соответствие элементам перечислений объектных свойств,
   // некорректные значения будут отброшены в методе detect монитора
   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];
      
      // делаем с помощью монитора бэкап свойств текущего объекта
      ObjectMonitor object(nameflags);
      object.print();
      object.backup();
      // формируем корректное, подходящее имя для копии
      const string copy = GetFreeName(name);
      
      if(StringLen(copy) > 0)
      {
         Print("Copy name: "copy);
         // создаем объект такого же типа OBJPROP_TYPE
         ObjectCreate(0copy,
            (ENUM_OBJECT)ObjectGetInteger(0nameOBJPROP_TYPE),
            ObjectFind(0name), 00);
         // меняем в мониторе имя объекта на новое
         object.name(copy);
         // восстанавливаем все свойства из бэкапа уже в новый объект
         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 №";
   // проверяем, нет ли в имени суффикса копии
   const int pos = StringFind(namesuffix);
   string prefix;
   int n;
   
   if(pos <= 0)
   {
      // если суффикс не найден, предполагаем копию под номером 1   
      const string candidate = name + suffix + "1";
      // проверяем свободно ли имя копии, и если да - возвращаем его 
      if(ObjectFind(0candidate) < 0)
      {
         return candidate;
      }
      // иначе готовимся к циклу с перебором номеров копий
      prefix = name;
      n = 0;
   }
   else
   {
      // если суффикс найден, выделяем название без него   
      prefix = StringSubstr(name0pos);
      // и находим в строке номер копии
      n = (int)StringToInteger(StringSubstr(namepos + StringLen(suffix)));
   }
   
   Print("Found: "prefix" "n);
   // пытаемся в цикле найти свободный номер копии выше n, но не более 1000
   for(int i = n + 1i < 1000; ++i)
   {
      const string candidate = prefix + suffix + (string)i;
      // проверяем существование объекта с именем с концовкой "Copy №i"
      if(ObjectFind(0candidate) < 0)
      {
         return candidate// возвращаем вакантное имя копии
      }
   }
   return NULL// слишком много копий
}

В принципе, терминал запоминает последние настройки конкретного типа объектов, и если их создавать один за другим, это эквивалентно копированию. Однако, настройки обычно меняются в процессе работы с разными графиками, и если по прошествии времени возникает необходимость продублировать какой-то "старый" объект, то настройки для него, как правило, приходится делать полностью. Особенно накладно это для типов объектов с большим количеством свойств, например, инструментов Фибоначчи. В таких случаях и пригодится данный скрипт.

Некоторые картинки из этой главы, на которых размещены однотипные объекты, были созданы с помощью данного скрипта.

Индикатор ObjectGroupEdit

Вторым примером использования 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.