Visión general de las funciones de acceso a las propiedades de los objetos

Los objetos tienen varios tipos de propiedades que pueden leerse y establecerse utilizando las funciones ObjectGet y ObjectSet. Como sabemos, este principio ya se ha aplicado al gráfico (véase la sección Visión general de las funciones para trabajar con el conjunto completo de propiedades de gráfico ).

Todas estas funciones toman como primeros tres parámetros un identificador de gráfico, un nombre de objeto y un identificador de propiedad, que debe pertenecer a una de las enumeraciones ENUM_OBJECT_PROPERTY_INTEGER, ENUM_OBJECT_PROPERTY_DOUBLE o ENUM_OBJECT_PROPERTY_STRING. Estudiaremos propiedades específicas gradualmente en las siguientes secciones. Sus tablas dinámicas completas se pueden encontrar en la documentación de MQL5, en la página con Propiedades de los objetos.

Cabe señalar que los identificadores de propiedad de las tres enumeraciones no se intersecan, lo que permite combinar su tratamiento conjunto en un único código unificado. Lo utilizaremos en los ejemplos.

Algunas propiedades son de sólo lectura y se marcarán con «r/o» (read-only).

Como en el caso de la API de trazado, las funciones de lectura de propiedades tienen una forma corta y una forma larga: la forma corta devuelve directamente el valor solicitado, y la forma larga devuelve un booleano de éxito (true) o errores (false), y el valor en sí se coloca en el último parámetro pasado por referencia. La ausencia de error al llamar al formulario corto debe comprobarse utilizando la variable incorporada _LastError.

Al acceder a algunas propiedades, debe especificar un parámetro adicional (modifier), que se utiliza para indicar el número de valor o el nivel si la propiedad es multivalorada. Por ejemplo, si un objeto tiene varios puntos de anclaje, el modificador permite seleccionar uno en concreto.

A continuación se muestran los prototipos de funciones para leer y escribir propiedades de enteros. Tenga en cuenta que el tipo de valores en ellos es long, lo que le permite almacenar propiedades no sólo de los tipos int o long, sino también bool, color, datetime y varias enumeraciones (véase más adelante).

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)

Las funciones para propiedades reales se describen de forma similar.

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)

Por último, existen cuatro funciones idénticas para las cadenas.

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)

Para mejorar el rendimiento, todas las funciones para establecer las propiedades de los objetos (ObjectSetInteger, ObjectSetDouble y ObjectSetString) son asíncronas y esencialmente envían comandos al gráfico para modificar el objeto. Una vez ejecutadas con éxito estas funciones, los comandos se colocan en la cola de eventos compartidos del gráfico, indicada por el resultado devuelto de true. Cuando se produce un error, las funciones devolverán false, y el código de error debe comprobarse en la variable _LastError.

Las propiedades de los objetos se modifican con cierto retraso, durante el procesamiento de la cola de eventos del gráfico. Para forzar la actualización del aspecto y las propiedades de los objetos del gráfico, especialmente después de cambiar muchos objetos a la vez, utilice la función ChartRedraw.

Las funciones para obtener las propiedades de los gráficos (ObjectGetInteger, ObjectGetDouble y ObjectGetString) son síncronas, es decir, el código de llamada espera el resultado de su ejecución. En este caso, se ejecutan todos los comandos de la cola de gráficos para obtener el valor real de las propiedades.

Volvamos al ejemplo del script para borrar objetos; más concretamente, a su nueva versión, ObjectCleanup2.mq5. Recordemos que en la función CustomDeleteAllObjects queríamos implementar la posibilidad de seleccionar objetos en función de sus propiedades. Digamos que estas propiedades deben ser el color y el punto de anclaje. Para obtenerlos, utilice la función ObjectGetInteger y un par de elementos de enumeración ENUM_OBJECT_PROPERTY_INTEGER: OBJPROP_COLOR y OBJPROP_ANCHOR. Los analizaremos en detalle más adelante.

Dada esta información, el código se completaría con las siguientes comprobaciones (aquí, por simplicidad, el color y el punto de anclaje vienen dados por las constantes clrRed y ANCHOR_TOP. De hecho, les proporcionaremos variables de entrada).

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;
}

Preste atención a las líneas con ObjectGetInteger.

Su entrada es larga y contiene cierta tautología porque las propiedades específicas están vinculadas a funciones ObjectGet de tipos conocidos. Además, a medida que aumenta el número de condiciones, puede parecer redundante repetir el ID del gráfico y el nombre del objeto.

Para simplificar el registro, pasemos a la tecnología que probamos en el archivo ChartModeMonitor.mqh en la sección sobre Modos de visualización de gráficos. Su significado es describir una clase intermediaria con sobrecargas de métodos para leer y escribir propiedades de todos los tipos. Pongamos nombre al nuevo archivo de encabezado ObjectMonitor.mqh.

La clase ObjectProxy reproduce fielmente la estructura de la clase ChartModeMonitorInterface para gráficos. La principal diferencia es la presencia de métodos virtuales para establecer y obtener el ID del gráfico y el nombre del objeto.

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) { }
};

Vamos a implementar estos métodos en la clase descendiente (más adelante complementaremos la jerarquía de clases con el monitor de propiedades del objeto, similar al monitor de propiedades del gráfico).

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;
   }
};

Hemos separado la interfaz abstracta ObjectProxy y su implementación mínima en ObjectSelector porque más adelante podemos necesitar implementar un array de proxies para múltiples objetos del mismo tipo, por ejemplo. Entonces basta con almacenar un array de nombres o su prefijo común en la nueva clase «multiselector» y asegurarse de que uno de ellos es devuelto desde el método name llamando al operador sobrecargado []:multiSelector[i].get(OBJPROP_XYZ).

Ahora volvamos al script ObjectCleanup2.mq5 y describamos dos variables de entrada para especificar un color y un punto de anclaje como condiciones adicionales para seleccionar los objetos que se van a eliminar.

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

Pasemos estos valores a la función CustomDeleteAllObjects, y las nuevas comprobaciones de condiciones en el bucle sobre objetos podrán formularse de forma más compacta gracias a la clase mediadora.

#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;
}

Es importante señalar que especificamos el nombre del objeto (y el identificador implícito del gráfico 0 actual) sólo una vez al crear el objeto ObjectSelector. Además, todas las propiedades se solicitan mediante el método get con un único parámetro que describe la propiedad deseada, y el compilador elegirá automáticamente la función ObjectGet adecuada.

La comprobación adicional del código de error 4203 (OBJECT_WRONG_PROPERTY) permite filtrar los objetos que no tienen la propiedad solicitada, como OBJPROP_ANCHOR. De este modo, en concreto, es posible hacer una selección en la que caerán todos los tipos de flechas (sin necesidad de solicitar por separado distintos tipos de OBJ_ARROW_XYZ), pero las líneas y los «eventos» quedarán excluidos del procesamiento.

Esto es fácil de comprobar ejecutando primero el script ObjectSimpleShowcase.mq5 en el gráfico (creará 14 objetos de diferentes tipos) y luego ObjectCleanup2.mq5. Si activa el modo UseCustomDeleteAll, habrá 5 objetos no borrados en el gráfico: OBJ_VLINE, OBJ_HLINE, OBJ_ARROW_BUY, OBJ_ARROW_SELL, and OBJ_EVENT. Los dos primeros y el último no tienen la propiedad OBJPROP_ANCHOR, y las flechas de compra y venta no pasan por color (se asume que el color de todos los demás objetos creados es rojo por defecto).

No obstante, ObjectSelector se proporciona no sólo por el bien de la sencilla aplicación anterior: es la base para crear un monitor de propiedades para un solo objeto, similar a lo que se implementó para los gráficos. Así, el archivo de encabezado ObjectMonitor.mqh contiene algo más interesante.

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) { }
};

Este conjunto de métodos debería recordarle ChartModeMonitorInterface de ChartModeMonitor.mqh. La única innovación es el método applyChanges, que copia las propiedades de un objeto a otro.

Basado en ObjectMonitorInterface, aquí está la descripción de la implementación básica de un monitor de propiedades para un par de tipos de plantilla: un tipo de valor de propiedad (uno de long, double o string) y el tipo de enumeración (uno de ENUM_OBJECT_PROPERTY_ish).

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
   ...

El constructor ObjectMonitorBase tiene dos parámetros: el nombre del objeto y un array de banderas con identificadores de las propiedades que deben observarse en el objeto especificado. Una parte importante de este código es casi idéntica a ChartModeMonitor. En concreto, como antes, se pasa un array de banderas al método de ayuda detect, cuyo propósito principal es identificar aquellas constantes de enteros que son elementos de la enumeración E, y eliminar el resto. El único añadido que hay que aclarar es la obtención de una propiedad con el número de niveles de un objeto a través de ObjectGetInteger(0, id, OBJPROP_LEVELS). Esto es necesario para admitir la iteración de propiedades con múltiples valores debido a la presencia de niveles (por ejemplo, Fibonacci). Para objetos sin niveles, obtendremos la cantidad 0, y tal propiedad será la escalar habitual.

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);
      }
   }
   ...

Por supuesto, el método detect es algo diferente de lo que vimos en ChartModeMonitor. Recordemos que, para empezar, contiene un fragmento con una comprobación de si la constante v pertenece a la enumeración E, mediante una llamada a la función EnumToString: si no existe tal elemento en la enumeración, se generará un código de error. Si el elemento existe, añadimos el valor de la propiedad correspondiente al array 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;
   }

En el monitor de objetos, nos vemos obligados a complicar este esquema, ya que algunas propiedades son multivaloradas debido al parámetro modifier de las funciones ObjectGet y ObjectSet.

Así que introducimos un array estático modifiables con una lista de aquellas propiedades que los modificadores admiten (cada propiedad será discutida en detalle más adelante). La conclusión es que, para este tipo de propiedades multivaloradas, es necesario leerlas y almacenarlas en el array data no una vez, sino varias.

// 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
      };
      ...

Aquí también utilizamos el truco con EnumToString para comprobar la existencia de una propiedad con el identificador v. Si tiene éxito, comprobamos si está en la lista de modifiables y establecemos la correspondiente bandera modifiable en true o 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 = 0i < ArraySize(modifiables); ++i)
         {
            if(v == modifiables[i])
            {
               modifiable = true;
               break;
            }
         }
         ...

Por defecto, cualquier propiedad se considera inequívoca y, por lo tanto, el número necesario de lecturas a través de la función ObjectGet o de entradas a través de la función ObjectSet es igual a 1 (la variable k a continuación).

         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;
         }

Si un objeto admite niveles, limitamos el número potencial de lecturas/escrituras con el parámetro levels (como recordamos, se obtiene en el código de llamada a partir de la propiedad OBJPROP_LEVELS).

Para la propiedad OBJPROP_BMPFILE, como pronto aprenderemos, sólo se permiten dos estados: activado (botón pulsado, bandera activada) o desactivado (botón liberado, bandera desactivada), por lo que k = 2.

Por último, las coordenadas de objeto (OBJPROP_TIME y OBJPROP_PRICE) son convenientes porque generan un error cuando se intenta leer/escribir un punto de anclaje inexistente. Por lo tanto, asignamos a k algún valor obviamente grande de MOD_MAX, y entonces podemos interrumpir el ciclo de lectura de puntos en un valor distinto de cero _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;
   }

Dado que una propiedad puede tener varios valores, que se leen en un bucle hasta k, ya no podemos escribir simplemente data.put((E)v, get((E)v)). Necesitamos combinar de alguna manera el identificador de la propiedad v y su número de modificación i. Afortunadamente, el número de propiedades también está limitado en una constante de entero (tipo int): no se ocupan más de dos bytes inferiores. Así que podemos utilizar operadores a nivel de bits para poner i al byte superior. Para ello se ha desarrollado la macro MOD_COMBINE.

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

Por supuesto, se proporcionan macros inversas para recuperar el ID de la propiedad y el número de revisión.

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

Por ejemplo, aquí podemos ver cómo se utilizan en el método 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;
   }

Este método repite toda la lógica del método del mismo nombre en ChartModeMonitor.mqh; sin embargo, para leer propiedades en todas partes, primero debe extraer el nombre de la propiedad de la clave almacenada usando MOD_GET_NAME y el número usando MOD_GET_INDEX.

Una complicación similar tiene que hacerse en el método 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);
      }
   }

La innovación más interesante de ObjectMonitorBase es cómo funciona con los cambios.

   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);
            }
         }
      }
   }

Si pasamos al método applyChanges de estados del monitor de otro objeto, podemos adoptar todos los últimos cambios del mismo.

Para admitir propiedades de los tres tipos básicos (long,double,string) necesitamos implementar la clase ObjectMonitor (análoga a ChartModeMonitor de 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);
   }
   ...

Aquí también se conserva la estructura de código anterior, y sólo se han añadido métodos para admitir cambios y nombres (los gráficos, como recordamos, no tienen nombres).

   ...
   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));
      }
   }

Basándose en el monitor de objetos creado, es fácil implementar varios trucos que no se admiten en el terminal. En concreto, se trata de la creación de copias de objetos y la edición en grupo de objetos.

Script ObjectCopy

El script ObjectCopy.mq5 demuestra cómo copiar objetos seleccionados. Al principio de su función OnStart, llenamos el array flags con enteros consecutivos que son candidatos a elementos de enumeraciones ENUM_OBJECT_PROPERTY_ de diferentes tipos. La numeración de los elementos de enumeración tiene una marcada agrupación por finalidad, y hay grandes espacios entre los grupos (aparentemente, un margen para futuros elementos), por lo que el array formado es bastante grande: 2048 elementos.

#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;
   }
   ...

A continuación, recopilamos en un array los nombres de los objetos que están seleccionados actualmente en el gráfico. Para ello, utilizamos la propiedad 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);
      }
   }
   ...

Por último, en el bucle principal sobre los elementos seleccionados, leemos las propiedades de cada objeto, formamos el nombre de su copia y creamos un objeto bajo él con el mismo conjunto de atributos.

   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);
      }
   }
}

Es importante señalar aquí que la propiedad OBJPROP_TYPE es una de las pocas propiedades de sólo lectura, y por lo tanto es vital crear un objeto del tipo requerido para empezar.

La función de ayuda GetFreeName intenta añadir la cadena «/Copy #x» al nombre del objeto, donde x es el número de copia. Así, ejecutando el script varias veces, puede crear la 2ª, 3ª y sucesivas copias.

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
}

El terminal recuerda la última configuración de un determinado tipo de objeto, y si se crean uno tras otro, esto equivale a copiar. No obstante, los ajustes suelen cambiar en el proceso de trabajo con diferentes gráficos, y si después de un tiempo hay necesidad de duplicar algún objeto «antiguo», entonces los ajustes para él, por regla general, tienen que hacerse por completo. Esto es especialmente costoso para los tipos de objetos con un gran número de propiedades, por ejemplo, las herramientas de Fibonacci. En tales casos, este script le resultará muy útil.

Algunas de las imágenes de este capítulo, que contienen objetos del mismo tipo, se han creado utilizando este script.

Indicador ObjectGroupEdit

El segundo ejemplo de utilización de ObjectMonitor es el indicador ObjectGroupEdit.mq5, que permite editar a la vez las propiedades de un grupo de objetos seleccionados.

Imaginemos que hemos seleccionado varios objetos en el gráfico (no necesariamente del mismo tipo), para los que es necesario cambiar uniformemente una u otra propiedad. A continuación, abrimos el cuadro de diálogo de propiedades de cualquiera de estos objetos, lo configuramos y, pulsando OK, estos cambios se aplican a todos los objetos seleccionados. Así es como funciona nuestro siguiente programa MQL.

Necesitábamos un indicador como tipo de programa porque implica eventos gráficos. Para este aspecto de la programación MQL5 dispondrá de un capítulo entero dedicado en exclusiva al respecto, pero ahora descubriremos algunos de los aspectos básicos.

Como el indicador no tiene gráficos, las directivas #property contienen ceros y la función OnCalculate está prácticamente vacía.

#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;
}

Para generar automáticamente un conjunto completo de todas las propiedades de un objeto, utilizaremos de nuevo un array de 2048 elementos con valores enteros consecutivos. También proporcionaremos un array para los nombres de los elementos seleccionados y un array de objetos monitor de la clase ObjectMonitor.

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

En el manejador OnInit, inicializamos el array de números y ponemos en marcha el temporizador.

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

En el manejador del temporizador, guardamos los nombres de los objetos seleccionados en un array. Si la lista de selección ha cambiado, es necesario reconfigurar los objetos monitor, para lo cual se llama a la función auxiliar 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();
   }
}

La función TrackSelectedObjects en sí es bastante sencilla: borrar los monitores antiguos y crear otros nuevos. Si lo desea, puede hacerlo más inteligente manteniendo la parte no modificada de la selección.

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));
   }
}

Recordemos que, cuando se crea un objeto monitor, se toma inmediatamente un «molde» de todas las propiedades del objeto gráfico correspondiente.

Ahora llegamos por fin a la parte en la que entran en juego los eventos. Como ya se mencionó en la visión general de las funciones de eventos, el manejador es responsable de los eventos OnChartEvent en el gráfico. En este ejemplo nos interesa un evento CHARTEVENT_OBJECT_CHANGE específico: ocurre cuando el usuario cambia cualquier atributo en el cuadro de diálogo de propiedades del objeto. El nombre del objeto modificado se pasa en el parámetro sparam.

Si este nombre coincide con uno de los objetos monitorizados, pedimos al monitor que haga una nueva instantánea de sus propiedades, es decir, llamamos a 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;
         }
      }
   }
}

Si los cambios se confirman (y es poco probable que no sea así), su número en la variable changes será mayor que 0. A continuación se inicia un bucle sobre todos los objetos seleccionados, y los cambios detectados se aplican a cada uno de ellos, excepto al original.

Dado que potencialmente podemos cambiar muchos objetos, llamamos a la petición de redibujado del gráfico con ChartRedraw.

En el manejador OnDeinit, eliminamos todos los monitores.

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

Eso es todo: la nueva herramienta está lista.

Este indicador permite personalizar el aspecto general de varios grupos de objetos de etiqueta en la sección sobre Definición del punto de anclaje del objeto.

Por cierto: de acuerdo con un principio similar con la ayuda de ObjectMonitor, usted puede hacer otra herramienta popular que no está disponible en el terminal: para deshacer ediciones a las propiedades del objeto, dado que el método restore ya está listo.