Ajustes de visualización de objetos: color, estilo y marco

La apariencia de los objetos puede modificarse mediante diversas propiedades, que exploraremos en esta sección, empezando por el color, el estilo, el ancho de línea y los bordes. Otros aspectos del formato, como el tipo de letra, la inclinación y la alineación del texto, se abordarán en las secciones siguientes.

Todas las propiedades de la siguiente tabla tienen tipos compatibles con enteros y, por tanto, son gestionadas por las funciones ObjectGetInteger y ObjectSetInteger.

Identificador

Descripción

Tipo de propiedad

OBJPROP_COLOR

El color de la línea y del elemento principal del objeto (por ejemplo, fuente o relleno)

color

OBJPROP_STYLE

Estilo de línea

ENUM_LINE_STYLE

OBJPROP_WIDTH

Grosor de la línea en píxeles

int

OBJPROP_FILL

Rellenar un objeto con color (para OBJ_RECTANGLE, OBJ_TRIANGLE, OBJ_ELLIPSE, OBJ_CHANNEL, OBJ_STDDEVCHANNEL, OBJ_REGRESSION)

bool

OBJPROP_BACK

Objeto en segundo plano

bool

OBJPROP_BGCOLOR

Color de fondo para OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL

color

OBJPROP_BORDER_TYPE

Tipo de marco para panel rectangular OBJ_RECTANGLE_LABEL

ENUM_BORDER_TYPE

OBJPROP_BORDER_COLOR

Color del marco para el campo de entrada OBJ_EDIT y el botón OBJ_BUTTON

color

A diferencia de la mayoría de los objetos con líneas (verticales y horizontales separadas, de tendencia, cíclicas, canales, etc.), donde la propiedad OBJPROP_COLOR define el color de la línea, para las imágenes OBJ_BITMAP_LABEL y OBJ_BITMAP define el color del marco, y OBJPROP_STYLE define el tipo de dibujo del marco.

Ya hemos conocido la enumeración ENUM_LINE_STYLE, utilizada para OBJPROP_STYLE, en el capítulo sobre indicadores, en la sección sobre Configuración de trazado.

Es necesario distinguir el relleno realizado por el color de primer plano OBJPROP_COLOR del color de fondo OBJPROP_BGCOLOR. Ambos son compatibles con diferentes grupos de tipos de objetos, que se enumeran en la tabla.

La propiedad OBJPROP_BACK requiere una explicación aparte. El hecho es que los objetos y los indicadores se muestran por defecto en la parte superior del gráfico de precios. El usuario puede cambiar este comportamiento para todo el gráfico yendo al cuadro de diálogo Setting del gráfico, y más allá del marcador Shared, la opción Chart on top. Este indicador también tiene un equivalente en el software, la propiedad CHART_FOREGROUND (véase Modos de visualización de gráficos). Sin embargo, a veces es conveniente no eliminar todos los objetos, sino sólo los seleccionados, en el fondo. A continuación, para ellos, puede establecer OBJPROP_BACK en true. En este caso, el objeto se solapará incluso con los separadores de cuadrícula y de punto, si están activados en el gráfico.

Cuando el modo de relleno OBJPROP_FILL está activado, el color de las barras que caen dentro de la forma depende de la propiedad OBJPROP_BACK. Por defecto, con OBJPROP_BACK igual a false, las barras que se superponen al objeto se dibujan en color invertido con respecto a OBJPROP_COLOR (el color invertido se obtiene cambiando todos los bits del valor de color por los opuestos, por ejemplo, se obtiene 0x00FF7F para 0xFF0080). Con OBJPROP_BACK igual a true, las barras se dibujan de la forma habitual, ya que el objeto se muestra en segundo plano, «debajo» del gráfico (véase un ejemplo más abajo).

La enumeración ENUM_BORDER_TYPE contiene los siguientes elementos:

Identificador

Apariencia

BORDER_FLAT

Plano

BORDER_RAISED

Convexo

BORDER_SUNKEN

Cóncavo

Cuando el borde es plano (BORDER_FLAT), se dibuja como una línea con color, estilo y anchura según las propiedades OBJPROP_COLOR, OBJPROP_STYLE, OBJPROP_WIDTH. Las versiones convexa y cóncava imitan chaflanes de volumen alrededor del perímetro en tonos de OBJPROP_BGCOLOR.

Cuando no se establece el color del borde OBJPROP_BORDER_COLOR (por defecto, que corresponde a clrNone), el campo de entrada queda enmarcado por una línea del color principal OBJPROP_COLOR, y alrededor del botón se dibuja un marco tridimensional con chaflanes en los tonos de OBJPROP_BGCOLOR.

Para probar las nuevas propiedades, vea el script ObjectStyle.mq5. En él, crearemos 5 rectángulos del tipo OBJ_RECTANGLE, es decir, con referencia al tiempo y a los precios. Estarán espaciados uniformemente por todo el ancho de la ventana, resaltando el rango entre el precio máximo High y el precio mínimo Low en cada uno de los cinco periodos de tiempo. Para todos los objetos ajustaremos y cambiaremos periódicamente el color, el estilo y el grosor de las líneas, así como la opción de relleno y visualización detrás del gráfico.

Vamos a utilizar de nuevo la clase auxiliar ObjectBuilder, derivada de Object Selector. A diferencia de lo que vimos en la sección anterior, añadimos a ObjectBuilder un destructor en el que llamaremos a ObjectDelete.

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

Esto permitirá asignar a esta clase no sólo la configuración de objetos, sino también su eliminación automática al finalizar el script.

En la función OnStart averiguamos el número de barras visibles y el índice de la primera barra, y también calculamos la anchura de un rectángulo en barras.

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

Reservemos un array de punteros inteligentes para los objetos con el fin de garantizar la llamada de los destructores ObjectBuilder.

   AutoPtr<ObjectBuilderobjects[OBJECT_NUMBER];

Defina una paleta de colores y cree 5 objetos de rectángulo.

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

Aquí, para cada objeto, se calculan las coordenadas de dos puntos de anclaje; se establecen el color, el estilo y el ancho de línea iniciales.

A continuación, en un bucle infinito, cambiamos las propiedades de los objetos. Cuando ScrollLock está activado, la animación puede pausarse.

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

Este es el aspecto que tiene en un gráfico:

Rectángulos OBJ_RECTANGLE con diferentes configuraciones de visualización

Rectángulos OBJ_RECTANGLE con diferentes configuraciones de visualización

El rectángulo rojo situado más a la izquierda tiene activado el modo de relleno y está en primer plano. Así, las barras de su interior se muestran en azul brillante contrastado (clrAqua, también conocido comúnmente como cyan, que es clrRed invertido ). El rectángulo púrpura también tiene un relleno, pero con una opción de fondo, por lo que las barras en él se muestran de una manera estándar.

Tenga en cuenta que el rectángulo naranja cubre completamente las barras al principio y al final de su subrango debido a la gran anchura de las líneas y a la visualización en la parte superior del gráfico.

Cuando el relleno está activado, no se tiene en cuenta la anchura de la línea. Cuando la anchura del borde es superior a 1, no se aplican algunos estilos de línea discontinua.

ObjectShapesDraw

Para el segundo ejemplo de esta sección, recuerde el hipotético programa de dibujo de formas que esbozamos en la Parte 3 cuando descubrimos POO. Nuestro progreso se detuvo en el hecho de que en el método de dibujo virtual (y se llamaba dibujar) sólo podíamos imprimir un mensaje en el registro de que estábamos dibujando una forma específica. Ahora, tras familiarizarnos con los objetos gráficos, tenemos la oportunidad de poner en práctica el dibujo.

Tomemos el script Shapes5stats.mq5 como punto de partida. La versión actualizada se llamará ObjectShapesDraw.mq5.

Recordemos que, además de la clase base Shape, hemos descrito varias clases de formas: Rectangle, Ellipse, Triangle, Square, Circle. Todas ellas superponen con éxito objetos gráficos de los tipos OBJ_RECTANGLE, OBJ_ELLIPSE, OBJ_TRIANGLE. Pero hay algunos matices.

Todos los objetos especificados están vinculados a coordenadas de tiempo y precio, mientras que nuestro programa de dibujo asume ejes X e Y unificados con posicionamiento puntual. A este respecto, necesitaremos configurar un gráfico para dibujar de forma especial y utilizar la función ChartXYToTimePrice para recalcular los puntos de la pantalla en tiempo y precio.

Además, los objetos OBJ_ELLIPSE y OBJ_TRIANGLE permiten rotaciones arbitrarias (en concreto, los radios pequeño y grande de una elipse pueden rotarse), mientras que OBJ_RECTANGLE siempre tiene sus lados orientados horizontal y verticalmente. Para simplificar el ejemplo, nos limitamos a la posición estándar de todas las formas.

En teoría, la nueva aplicación debería considerarse una demostración de objetos gráficos, y no un programa de dibujo. Un enfoque más correcto para el dibujo completo, desprovisto de las restricciones que imponen los objetos gráficos (ya que están destinados a otros fines en general, como el marcado de gráficos), es utilizar recursos gráficos. Por ello, volveremos a replantearnos el programa de dibujo en el capítulo dedicado a los recursos.

En la nueva clase Shape vamos a deshacernos de la estructura anidada Pair con coordenadas de objetos: esta estructura servía para demostrar varios principios de la programación orientada a objetos (POO), pero ahora es más fácil devolver la descripción original de los campos int x, y directamente a la clase Shape. También añadiremos un campo con el nombre del objeto.

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

El campo name será necesario para establecer las propiedades de un objeto gráfico, así como para eliminarlo del gráfico, lo que es lógico hacer en el destructor.

Dado que los distintos tipos de formas requieren un número diferente de puntos o tamaños característicos, añadiremos el método setup, además del método virtual draw, en la interfaz Shape:

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

Recordemos que en el script hemos implementado una clase anidada Shape::Registrator, que se encargaba de contar el número de formas por tipo. Ha llegado el momento de confiarle algo más de responsabilidad para que funcione como una fábrica de formas. Las clases o métodos «fábrica» son buenos porque permiten crear objetos de diferentes clases de forma unificada.

Para ello, añadimos a Registrator un método para crear una forma con los parámetros que incluyen las coordenadas obligatorias del primer punto, un color, y un array de parámetros adicionales (cada forma será capaz de interpretarlo de acuerdo con sus propias reglas, y en el futuro, leer o escribir en un archivo).

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

El método es virtual abstracto porque ciertos tipos de formas sólo pueden ser creados por clases registradoras derivadas descritas en clases descendientes de Shape. Para simplificar la escritura de clases de generación de registro derivadas, introducimos una clase de plantilla MyRegistrator con una implementación del método create adecuada para todos los casos.

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

Aquí llamamos al constructor de alguna forma T previamente desconocida, la ajustamos llamando a setup y devolvemos una instancia al código de llamada.

Así es como se utiliza en la clase Rectangle, que tiene dos parámetros adicionales para la anchura y la altura.

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

Al crear una forma, su nombre contendrá no sólo el nombre de la clase (typename), sino también el número ordinal de la instancia, calculado en la llamada a r.increment().

Otras clases de formas se describen de forma similar.

Ahora es el momento de examinar el método draw para Rectangle. En él, traducimos un par de puntos (x,y) y (x + dx, y + dy) a coordenadas tiempo/precio utilizando ChartXYToTimePrice y creamos un objeto OBJ_RECTANGLE.

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

Por supuesto, no olvide establecer el color en OBJPROP_COLOR y el relleno en OBJPROP_FILL.

En el caso de la clase Square, no es necesario modificar nada en sí: basta con igualar dx y dy.

Para la clase Ellipse, dos opciones adicionales, dx y dy, determinan los radios pequeño y grande trazados en relación con el centro (x,y). En consecuencia, en el método draw calculamos 3 puntos de anclaje y creamos un objeto OBJ_ELLIPSE.

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

Circle es un caso especial de elipse con radios iguales.

Por último, en esta fase sólo se admiten los triángulos equiláteros: el tamaño del lado está contenido en un campo adicional dx. Le invitamos a conocer su métododraw en el código fuente de forma independiente.

El nuevo script generará, como antes, un número determinado de formas aleatorias. Se crean mediante la función addRandomShape.

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

Aquí es donde vemos el uso del método de fábrica create, llamado sobre un objeto registrador seleccionado aleatoriamente con el número n. Si más adelante decidimos añadir otras clases de formas, no tendremos que cambiar nada en la lógica de generación.

Todas las formas se colocan en la parte central de la ventana y tienen dimensiones no superiores a un cuarto de la ventana.

Queda por considerar directamente las llamadas a la función addRandomShape, y la configuración especial del horario que ya hemos mencionado.

Para obtener una representación «cuadrada» de los puntos en la pantalla, ajuste el modo CHART_SCALEFIX_11. Además, elegiremos la escala más densa (comprimida) a lo largo del eje temporal CHART_SCALE (0), porque en ella una barra ocupa 1 píxel horizontal (máxima precisión). Por último, desactive la visualización del propio gráfico estableciendo CHART_SHOW en false.

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

Para almacenar las formas, reservemos un array de punteros inteligentes y llenémoslo de formas aleatorias.

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

A continuación, ejecutamos un bucle infinito hasta que el usuario detiene el script, en el que movemos ligeramente las formas utilizando el método move.

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

Al final, restauramos la configuración del gráfico.

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

En la siguiente captura de pantalla se muestra el aspecto que podría tener un gráfico con las formas dibujadas.

Objetos de forma del gráfico
Objetos de forma del gráfico

La particularidad de dibujar objetos es la «multiplicación» de los colores en los lugares donde se superponen.

Debido a que el eje Y sube y baja, todos los triángulos están al revés, pero eso no es crítico, porque vamos a rehacer el programa de pintura basado en recursos de todos modos.