Настройка отображения объекта: цвет, стиль и рамка

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

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

Идентификатор

Описание

Тип свойства

OBJPROP_COLOR

Цвет линии и основного элемента объекта (например, шрифта или заливки)

color

OBJPROP_STYLE

Стиль линии

ENUM_LINE_STYLE

OBJPROP_WIDTH

Толщина линии в пикселях

int

OBJPROP_FILL

Заливка объекта цветом (для OBJ_RECTANGLE, OBJ_TRIANGLE, OBJ_ELLIPSE, OBJ_CHANNEL, OBJ_STDDEVCHANNEL, OBJ_REGRESSION)

bool

OBJPROP_BACK

Объект на заднем плане

bool

OBJPROP_BGCOLOR

Цвет фона для OBJ_EDIT, OBJ_BUTTON, OBJ_RECTANGLE_LABEL

color

OBJPROP_BORDER_TYPE

Тип рамки для прямоугольной панели OBJ_RECTANGLE_LABEL

ENUM_BORDER_TYPE

OBJPROP_BORDER_COLOR

Цвет рамки для поля ввода OBJ_EDIT и кнопки OBJ_BUTTON

color

В отличие от большинства объектов с линиями (отдельные вертикальная и горизонтальная, трендовая, циклические, каналы и т.д.), где свойство OBJPROP_COLOR определяет цвет линии, для картинок OBJ_BITMAP_LABEL и OBJ_BITMAP оно определяет цвет рамки, а OBJPROP_STYLE — тип отрисовки рамки.

Перечисление ENUM_LINE_STYLE, используемое для OBJPROP_STYLE, мы уже встречали в главе про индикаторы, в разделе Настройка графических построений.

Следует отличать заливку, выполняемую основным цветом OBJPROP_COLOR, от цвета фона OBJPROP_BGCOLOR. И то, и другое поддерживается разными группами типов объектов — они перечислены в таблице.

Свойство OBJPROP_BACK требует отдельных пояснений. Дело в том, что объекты и индикаторы по умолчанию выводятся поверх графика цен. Пользователь может изменить такое поведение для всего графика целиком, зайдя в диалог Настройка графика и далее закладка Общие, опция График сверху. Этот флажок имеет и программный аналог — свойство CHART_FOREGROUND (см. Режимы отображения графика). Однако иногда желательно убрать на задний план не все объекты, а лишь избранные. Тогда для них можно установить OBJPROP_BACK в true. При этом объект будет перекрываться даже сеткой и разделителями периодов, если они включены на графике.

Когда включен режим заливки OBJPROP_FILL, цвет баров, попадающих внутрь фигуры, зависит от свойства OBJPROP_BACK. По умолчанию, при OBJPROP_BACK равном false, бары, наложившиеся на объект, рисуются инвертированным цветом по отношению к OBJPROP_COLOR (инвертированный цвет получается переключением всех битов в значении цвета на противоположные, например, для 0xFF0080 получается 0x00FF7F). При OBJPROP_BACK равном true, бары рисуются обычным образом, поскольку объект выводится на заднем фоне, "под" графиком (см. пример далее).

Перечисление ENUM_BORDER_TYPE содержит следующие элементы:

Идентификатор

Внешний вид

BORDER_FLAT

Плоский

BORDER_RAISED

Выпуклый

BORDER_SUNKEN

Вогнутый

Когда рамка плоская (BORDER_FLAT), она отображается линией с цветом, стилем и шириной, согласно свойствам OBJPROP_COLOR, OBJPROP_STYLE, OBJPROP_WIDTH. Выпуклый и вогнутый варианты имитируют объемные фаски по периметру в оттенках OBJPROP_BGCOLOR.

Когда цвет рамки OBJPROP_BORDER_COLOR не задан (по умолчанию, что соответствует clrNone), поле ввода обрамляется линией основного цвета OBJPROP_COLOR, а вокруг кнопки рисуется объемная рамка с фасками в оттенках OBJPROP_BGCOLOR.

Для проверки работы новых свойств рассмотрим скрипт ObjectStyle.mq5. В нем мы создадим 5 прямоугольников типа OBJ_RECTANGLE, то есть с привязкой ко времени и ценам. Они будут равномерно расположены по всей ширине окна, подсвечивая диапазон между максимальной ценой High и минимальной ценой Low в каждом из пяти временных отрезков. Для всех объектов будем настраивать и периодически менять цвет, стиль, толщину линий, а также заполнение и опцию отображения под графиком.

Вновь воспользуемся вспомогательным классом ObjectBuilder, производным от ObjectSelector. В отличие от предыдущего раздела, добавим в ObjectBuilder деструктор, в котором вызовем ObjectDelete.

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

Это позволит возложить на этот класс не только настройку объектов, но и их автоматическое удаление по завершении работы скрипта.

В функции OnStart узнаем количество видимых баров, номер первого бара, а также вычисляем ширину одного прямоугольника в барах.

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

Зарезервируем под объекты массив умных указателей, чтобы обеспечить вызов деструкторов ObjectBuilder.

   AutoPtr<ObjectBuilder> objects[OBJECT_NUMBER];

Определим палитру цветов и создадим 5 объектов-прямоугольников.

   color colors[OBJECT_NUMBER] = {clrRed, clrGreen, clrBlue, clrMagenta, clrOrange};
   
   for(int i = 0; i < OBJECT_NUMBER; ++i)
   {
      // находим индексы баров, определяющих размах цен в i-м временном поддиапазоне
      const int h = iHighest(NULL, 0, MODE_HIGH, rectsize, i * rectsize);
      const int l = iLowest(NULL, 0, MODE_LOW, rectsize, i * rectsize);
      // создаем и настриваем объект в i-м поддиапазоне
      ObjectBuilder *object = new ObjectBuilder(name + (string)(i + 1), OBJ_RECTANGLE);
      object.set(OBJPROP_TIME, iTime(NULL, 0, i * rectsize), 0);
      object.set(OBJPROP_TIME, iTime(NULL, 0, (i + 1) * rectsize), 1);
      object.set(OBJPROP_PRICE, iHigh(NULL, 0, h), 0);
      object.set(OBJPROP_PRICE, iLow(NULL, 0, l), 1);
      object.set(OBJPROP_COLOR, colors[i]);
      object.set(OBJPROP_WIDTH, i + 1);
      object.set(OBJPROP_STYLE, (ENUM_LINE_STYLE)i);
      // сохраняем в массив
      objects[i] = object;
   }
   ...

Здесь для каждого объекта вычисляются координаты двух точек привязки, устанавливаются начальные цвет, стиль и ширина линий.

Далее в бесконечном цикле меняем свойства объектов. При включении ScrollLock анимацию можно приостановить.

   const int key = TerminalInfoInteger(TERMINAL_KEYSTATE_SCRLOCK);
   int pass = 0;
   int offset = 0;
   
   for( ;!IsStopped(); ++pass)
   {
      Sleep(200);
      if(TerminalInfoInteger(TERMINAL_KEYSTATE_SCRLOCK) != key) continue;
      // время от времени меняем цвет/стиль/ширину/заливку/фоновое отображение
      if(pass % 5 == 0)
      {
         ++offset;
         for(int i = 0; i < OBJECT_NUMBER; ++i)
         {
            objects[i][].set(OBJPROP_COLOR, colors[(i + offset) % OBJECT_NUMBER]);
            objects[i][].set(OBJPROP_WIDTH, (i + offset) % OBJECT_NUMBER + 1);
            objects[i][].set(OBJPROP_FILL, rand() > 32768 / 2);
            objects[i][].set(OBJPROP_BACK, rand() > 32768 / 2);
         }
      }
      ChartRedraw();
   }

Вот как это выглядит на графике.

Прямоугольники OBJ_RECTANGLE с разными настройками отображения

Прямоугольники OBJ_RECTANGLE с разными настройками отображения

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

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

При включенной заливке ширина линии не учитывается. При ширине границы больше 1 некоторые прерывистые стили линий не применяются.

ObjectShapesDraw

Для второго примера данного раздела вспомним о гипотетической программе рисования фигур, которую мы схематично проектировали в третьей Части, когда изучали ООП. Наш прогресс остановился на том, что в виртуальном методе рисования (а он так и назывался — draw) мы могли лишь вывести сообщение в журнал о том, что рисуем конкретную фигуру. Теперь, после знакомства с графическими объектами, у нас появилась возможность реализовать рисование.

В качестве отправной точки возьмем скрипт Shapes5stats.mq5. Дополненная версия будет называться ObjectShapesDraw.mq5.

Напомним, что помимо базового класса Shape у нас описано несколько классов фигур: Rectangle, Ellipse, Triangle, Square, Circle. Все они удачно ложатся на графические объекты типов OBJ_RECTANGLE, OBJ_ELLIPSE, OBJ_TRIANGLE. Но есть и некоторые нюансы.

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

Кроме того, объекты OBJ_ELLIPSE и OBJ_TRIANGLE допускают произвольное вращение (в частности, малый и большой радиус эллипса могут быть повернуты), в то время как OBJ_RECTANGLE всегда имеет свои стороны ориентированными по горизонтали и вертикали. Мы для упрощения примера ограничимся стандартным положением всех фигур.

В принципе, новую реализацию следует рассматривать именно как демонстрацию графических объектов, а не программы рисования. Более правильным подходом для полноценного рисования, лишенного ограничений, которые накладывают графические объекты (поскольку они предназначены в общем-то для других целей — разметки графика), является использование графических ресурсов. Поэтому мы вернемся к переосмыслению программы рисования в главе про ресурсы.

В новом классе Shape избавимся от вложенной структуры Pair с координатами объекта: эта структура служила средством демонстрации нескольких принципов ООП, но сейчас проще вернуть изначальное описание полей int x, y непосредственно в класс Shape. Также мы добавим поле с названием объекта.

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

Поле name потребуется для установки свойств графического объекта, а также для его удаления с графика, что логично сделать в деструкторе.

Поскольку разные типы фигур требуют разного количества точек или характерных размеров, добавим в интерфейс Shape в дополнение к виртуальному методу draw еще один — setup:

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

Напомним, что у нас в скрипте реализован вложенный класс Shape::Registrator, который занимался подсчетом количества фигур по типам. Настало время поручить ему кое-что более ответственное — работать в качестве фабрики фигур. Так называемые "фабричные" классы или методы хороши тем, что позволяют унифицированным образом создавать объекты разных классов.

Для этого добавим в Registrator метод для создания фигуры — с параметрами, включающими обязательные координаты первой точки, цвет и массив дополнительных параметров (каждая фигура сможет интерпретировать его по своим правилам, а в перспективе считывать из или записывать в файл).

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

Метод является абстрактным виртуальным, потому что конкретные типы фигур смогут создавать только производные классы-регистраторы, описываемые в классах-наследниках Shape. Чтобы упростить написание производных классов-регистраторов, введем шаблонный класс MyRegistrator с подходящей для всех случаев реализацией метода create.

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

Здесь мы вызываем конструктор некоей заранее неизвестной фигуры T, донастраиваем её с помощью вызова setup и возвращаем экземпляр вызывающему коду.

Вот как это используется в классе Rectangle, у которого два дополнительных параметра обозначают ширину и высоту.

class Rectangle : public Shape
{
   static MyRegistrator<Rectangle> r;
   
protected:
   int dx, dy; // размеры (ширины, высота)
   
   Rectangle(int px, int py, color back, string t) :
      Shape(px, py, back, t), dx(1), dy(1)
   {
   }
   
public:
   Rectangle(int px, int py, color back) :
      Shape(px, py, back, typename(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<Rectangle> Rectangle::r;

При создании фигуры её имя будет содержать не только имя класса (typename), но и порядковый номер экзмепляра, подсчитываемый в вызове r.increment().

Другие классы фигур описаны аналогично.

Теперь настало время заглянуть в метод draw для Rectangle. В нем мы переводим пару точек (x,y) и (x + dx, y + dy) в координаты время/цена с помощью ChartXYToTimePrice и создаем объект OBJ_RECTANGLE.

   void draw() override
   {
      // Print("Drawing rectangle");
      int subw;
      datetime t;
      double p;
      ChartXYToTimePrice(0, x, y, subw, t, p);
      ObjectCreate(0, name, OBJ_RECTANGLE, 0, t, p);
      ChartXYToTimePrice(0, x + dx, y + dy, subw, t, p);
      ObjectSetInteger(0, name, OBJPROP_TIME, 1, t);
      ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p);
   
      ObjectSetInteger(0, name, OBJPROP_COLOR, backgroundColor);
      ObjectSetInteger(0, name, OBJPROP_FILL, true);
   }

Разумеется, не забываем установить цвет OBJPROP_COLOR и заливку OBJPROP_FILL.

Для класса Square практически ничего не требуется менять: достаточно лишь установить равными dx и dy.

Для класса Ellipse два дополнительных параметра dx и dy определяют малый и большой радиусы, откладываемые относительно центра (x,y). Соответственно, в методе draw мы рассчитываем 3 точки привязки и создаем объект OBJ_ELLIPSE.

class Ellipse : public Shape
{
   static MyRegistrator<Ellipse> r;
protected:
   int dx, dy; // большой и малый радиусы
   ...
public:
   void draw() override
   {
      // Print("Drawing ellipse");
      int subw;
      datetime t;
      double p;
      
      // (x, y) центр
      // p0: x + dx, y
      // p1: x - dx, y
      // p2: x, y + dy
      
      ChartXYToTimePrice(0, x + dx, y, subw, t, p);
      ObjectCreate(0, name, OBJ_ELLIPSE, 0, t, p);
      ChartXYToTimePrice(0, x - dx, y, subw, t, p);
      ObjectSetInteger(0, name, OBJPROP_TIME, 1, t);
      ObjectSetDouble(0, name, OBJPROP_PRICE, 1, p);
      ChartXYToTimePrice(0, x, y + dy, subw, t, p);
      ObjectSetInteger(0, name, OBJPROP_TIME, 2, t);
      ObjectSetDouble(0, name, OBJPROP_PRICE, 2, p);
      
      ObjectSetInteger(0, name, OBJPROP_COLOR, backgroundColor);
      ObjectSetInteger(0, name, OBJPROP_FILL, true);
   }
};
   
static MyRegistrator<Ellipse> Ellipse::r;

Circle — это частный случай эллипса с равными радиусами.

Наконец, треугольники у нас поддерживаются на данном этапе только равносторонние: размер стороны содержится в дополнительном поле dx. С их методом draw предлагается ознакомиться в исходном коде самостоятельно.

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

Shape *addRandomShape()
{
   const int w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   const int h = (int)ChartGetInteger(0, CHART_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(cx, cy, clr, custom);
}

Именно здесь мы видим использование фабричного метода create, вызываемого у случайно выбранного объекта-регистратора под номером n. Если мы решим позднее добавить другие классы фигур, нам не потребуется ничего менять в логике генерации.

Все фигуры размещаются в центральной части окна и имеют размеры не больше четверти окна.

Осталось рассмотреть непосредственно вызовы функции addRandomShape и особую настройку графика, которую мы уже упоминали.

Для обеспечения "квадратного" представления точек на экране установим режим CHART_SCALEFIX_11. Кроме того выберем самый плотный (сжатый) масштаб по оси времени CHART_SCALE (0), потому что в нем один бар занимает 1 пиксель по горизонтали (максимальная точность). Наконец, отключим отображение самого графика, поставив CHART_SHOW в false.

void OnStart()
{
   const int scale = (int)ChartGetInteger(0, CHART_SCALE);
   ChartSetInteger(0, CHART_SCALEFIX_11, true);
   ChartSetInteger(0, CHART_SCALE, 0);
   ChartSetInteger(0, CHART_SHOW, false);
   ChartRedraw();
   ...

Для хранения фигур зарезервируем массив умных указателей и заполним его случайными фигурами.

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

Затем запускаем бесконечный цикл, пока пользователь не остановит скрипт, в котором слегка двигаем фигуры с помощью метода move.

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

В конце восстанавливаем настройки графика.

   // недостаточно отключить CHART_SCALEFIX_11, нужно CHART_SCALEFIX
   ChartSetInteger(0, CHART_SCALEFIX, false);
   ChartSetInteger(0, CHART_SCALE, scale);
   ChartSetInteger(0, CHART_SHOW, true);
}

На следующем скриншоте показано, как может выглядеть график с нарисованными фигурами.

Объекты-фигуры на графике
Объекты-фигуры на графике

Особенностью отрисовки объектов является "умножение" цветов в тех местах, где они перекрываются.

Поскольку ось Y идет сверху вниз, все треугольники получаются перевернутыми, но это не критично, потому что мы в любом случае собираемся переделать программу рисования на базе ресурсов.