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

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

Все свойства из нижеприведенной таблицы имеют типы, совместимые с целыми числами, в связи с чем управляются функциями 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 ObjectBuilderpublic ObjectSelector
{
...
public:
   ~ObjectBuilder()
   {
      ObjectDelete(hostid);
   }
   ...
};

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

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

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

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

   AutoPtr<ObjectBuilderobjects[OBJECT_NUMBER];

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

   color colors[OBJECT_NUMBER] = {clrRedclrGreenclrBlueclrMagentaclrOrange};
   
   for(int i = 0i < OBJECT_NUMBER; ++i)
   {
      // находим индексы баров, определяющих размах цен в i-м временном поддиапазоне
      const int h = iHighest(NULL0MODE_HIGHrectsizei * rectsize);
      const int l = iLowest(NULL0MODE_LOWrectsizei * rectsize);
      // создаем и настриваем объект в i-м поддиапазоне
      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);
      // сохраняем в массив
      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) != keycontinue;
      // время от времени меняем цвет/стиль/ширину/заливку/фоновое отображение
      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();
   }

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

Прямоугольники 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 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);
   }
   ...

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

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

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

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

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

virtual Shape *create(const int pxconst int pyconst 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 pxconst int pyconst color back,
      const int &parameters[]) override
   {
      T *temp = new T(pxpyback);
      temp.setup(parameters);
      return temp;
   }
};

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

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

class Rectangle : public Shape
{
   static MyRegistrator<Rectangler;
   
protected:
   int dxdy// размеры (ширины, высота)
   
   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;

При создании фигуры её имя будет содержать не только имя класса (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(0xysubwtp);
      ObjectCreate(0nameOBJ_RECTANGLE0tp);
      ChartXYToTimePrice(0x + dxy + dysubwtp);
      ObjectSetInteger(0nameOBJPROP_TIME1t);
      ObjectSetDouble(0nameOBJPROP_PRICE1p);
   
      ObjectSetInteger(0nameOBJPROP_COLORbackgroundColor);
      ObjectSetInteger(0nameOBJPROP_FILLtrue);
   }

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

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

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

class Ellipse : public Shape
{
   static MyRegistrator<Ellipser;
protected:
   int dxdy// большой и малый радиусы
   ...
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(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 — это частный случай эллипса с равными радиусами.

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

Новый скрипт будет, как и прежде, генерировать заданное количество случайных фигур. Их созданием занимается функция 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);
}

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

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

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

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

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

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

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

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

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

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

   // недостаточно отключить CHART_SCALEFIX_11, нужно CHART_SCALEFIX
   ChartSetInteger(0CHART_SCALEFIXfalse);
   ChartSetInteger(0CHART_SCALEscale);
   ChartSetInteger(0CHART_SHOWtrue);
}

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

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

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

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