Индикаторы на основе класса CCanvas: Заполнение каналов прозрачностью

Samuel Manoel De Souza | 3 мая, 2023

Введение

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

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

Темы статьи перечислены ниже:

Причины использования класса CCanvas

Кто-то может спросить, зачем использовать CCanvas, ведь для пользовательских индикаторов есть DRAW_FILLING. Есть как минимум две причины:

  1. Цвета индикатора смешиваются с цветами других индикаторов, свечей и объектов графика.
  2. DRAW_FILLING не допускает прозрачности

График с двумя индикаторами и одним объектом

Свойства окна графика

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

Свойства, которые мы собираемся использовать, перечислены ниже с кратким описанием. Если нам понадобится что-то еще, я упомяну об этом отдельно.

Понимание свойств окна графика

Эти свойства хорошо видны на следующем рисунке.

Свойства графика, связанные с координатами

Мы используем свойства CHART_WIDTH_IN_PIXELS и CHART_HEIGTH _IN_PIXELS, чтобы определить необходимый размер холста. При изменении размеров окна и изменении свойств, необходимо настроить размер холста.

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


Индикатор просмотра свойств графика

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

Я создал свой индикатор по указанному ниже пути. В организационных целях предлагаю вам сделать то же самое.

Когда скелет индикатора готов, нам нужно добавить в файл библиотеку CCanvas. Мы можем сделать это с помощью директивы #include.

Затем мы создаем экземпляр класса CCanvas. Всё это располагается после директив #property индикатора.

#property copyright "Copyright 2023, Samuel Manoel De Souza"
#property link      "https://www.mql5.com/en/users/samuelmnl"
#property version   "1.00"
#property indicator_chart_window

#include <Canvas/Canvas.mqh>
CCanvas Canvas;

Первое, что нам нужно сделать при работе с CCanvas, это создать OBJ_BITMAP_LABEL и добавить к нему ресурс. Это нужно сделать, если вы хотите добавить его на график, как правило, в блоке инициализации индикатора, используя метод CreateBitampLabel(...). Наконец необходимо удалить OBJ_BITMAP_LABEL и прикрепленный к нему ресурс. Это нужно сделать, если вы хотите удалить его с графика, как правило, в блоке деинициализации, используя метод Destroy(void). Тем временем мы выполняем основную отрисовку, которая состоит из стирания рисунков (очистка или установка значений пикселей по умолчанию для ресурса), создания рисунков и обновления ресурса. Полный жизненный цикл холста показан на диаграмме ниже.

canvas_process

Для простоты мы будем хранить Erase, Draw и Update в одной функции под названием Redraw. Прописывая все в коде, мы получаем следующую структуру.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   Canvas.CreateBitmapLabel(0, 0, "Canvas", 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Canvas.Destroy();
  }
//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = ColorToARGB(clrBlack);
   uint text_color = ColorToARGB(clrWhite);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw

//--- add second draw

//--- add ... draw

//--- add last draw

//--- canvas update
   Canvas.Update();
  }

Чтобы отобразить свойства, напишем их, используя метод TextOut. Значения этих свойств будут храниться в виде строки в переменной struct

struct StrProperty
  {
   string name;
   string value;
  };
Структура может быть следующей. Затем мы можем кратко вывести их в цикле. Поскольку у нас еще нет массива, передадим массив в качестве параметра функции Redraw. Функция Redraw выглядит так:
void Redraw(StrProperty &array[])
  {
   uint default_color = ColorToARGB(clrBlack);
   uint text_color = ColorToARGB(clrWhite);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   int total = ArraySize(array);
   for(int i=0;i<total;i++)
     {
      int padding = 2;
      int left = padding, right = Canvas.Width() - padding, y = i * 20 + padding;
      Canvas.TextOut(left, y, array[i].name, text_color, TA_LEFT);
      Canvas.TextOut(right, y, array[i].value, text_color, TA_RIGHT);
     }
//--- canvas update
   Canvas.Update();
  }
Наконец, мы можем получить значения свойств и вывести их. Если в вашем коде нет обработчика функции OnChartEvent, вам нужно добавить его. Там мы проверим идентификатор события CHARTEVENT_CHART_CHANGE. Когда возникает событие, мы объявляем некоторые переменные, которые принимают значения свойств и передают их в массив структур, а затем вызывают функцию Redraw. Мы можем скомпилировать индикатор, добавить его на график и манипулировать графиком, чтобы увидеть обновления холста.
//+------------------------------------------------------------------+
//| Custom indicator chart event handler function                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);
   int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN);
   double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX);
//---
   StrProperty array[]
     {
        {"Width", (string)chart_width},
        {"Height", (string)chart_height},
        {"Scale", (string)chart_scale},
        {"First Vis. Bar", (string)chart_first_vis_bar},
        {"Visible Bars", (string)chart_vis_bars},
        {"Price Min", (string)chart_prcmin},
        {"Price Max", (string)chart_prcmax},
     };
   Redraw(array);
  }

Преобразование координат

На этом этапе нам нужны некоторые базовые функции для преобразования даты и времени или индекса бара в x в пикселях, из цены в y в пикселях, из x в индекс бара и из y в цену (некоторые из них мы сейчас не будем использовать, но мы можем сделать их все сразу). В связи с этим перенесем переменные свойств диаграммы в глобальную область, в то время как в функции OnChartEvent мы будем только обновлять значения и вызывать функцию Redraw при необходимости. Идеальное решение — инкапсулировать переменные и функции преобразования в класс или структуру, но пока не будем усложнять. Я предлагаю вам начать изучение ООП с прочтения статьи "Основы объектно-ориентированного программирования" и соответствующей темы документации (Объектно-ориентированное программирование). Мы воспользуемся этим при следующей возможности.

Функции в основном связаны с пропорциональными отношениями.

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int BarWidth(int scale) {return (int)pow(2, scale);}
//+------------------------------------------------------------------+
//| Converts the bar index(as series) to x in pixels                 |
//+------------------------------------------------------------------+
int ShiftToX(int shift) {return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - 1;}
//+------------------------------------------------------------------+
//| Converts the price to y in pixels                                |
//+------------------------------------------------------------------+
int PriceToY(double price)
  {
// avoid zero divider
   if(chart_prcmax - chart_prcmin == 0.)
      return 0.;
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1);
  }
//+------------------------------------------------------------------+
//| Converts x in pixels to bar index(as series)                     |
//+------------------------------------------------------------------+
int XToShift(int x)
  {
// avoid zero divider
   if(BarWidth(chart_scale) == 0)
      return 0;
   return chart_first_vis_bar - (x + BarWidth(chart_scale) / 2) / BarWidth(chart_scale);
  }
//+------------------------------------------------------------------+
//| Converts y in pixels to price                                    |
//+------------------------------------------------------------------+
double YToPrice(int y)
  {
// avoid zero divider
   if(chart_height == 0)
      return 0;
   return chart_prcmax - y * (chart_prcmax - chart_prcmin) / chart_height;
  }

DRAW_FILLING с прозрачностью

Теперь у нас есть всё необходимое для реализации нашего DRAW_FILLING с помощью CCanvas.

Мы не будем тратить время на создание нового индикатора. Вместо этого возьмем пример, использующийся в платформе MetaTrader 5, и добавим заливку между двумя линиями. Я использую индикатор Envelopes в \\MQL5\\Indicators\\Examples\\, расположенном в папке данных терминала. Я скопирую Envelopes.mq5 в тот же каталог, где я создал индикатор ChartPropertiesViwer. Вы можете выбрать любой индикатор, но я предлагаю использовать тот же индикатор, следуя шагам, описанным в этой статье.

Первое, что нам нужно сделать, это скопировать все, что мы сделали в индикаторе ChartPropertiesViewer, в Envelopes.

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

double                   ExtUpBuffer[];
double                   ExtDownBuffer[];

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

Параметры Описание для переменной
 serie1  Массив значений, соответствующих первой линии
 serie2  Массив значений, соответствующих второй линии
 clr1  Цвет при serie1 >= serie2
 clr2  Цвет при serie1 < serie2
 alpha  Значение прозрачности канала
 plot_shift  Сдвиг индикатора вправо или влево от графика

Функция, использующая существующие переменные и упомянутые параметры, выглядит следующим образом.

//+------------------------------------------------------------------+
//| Fill the area between two lines                                  |
//+------------------------------------------------------------------+
void DrawFilling(double &serie1[], double &serie2[], color clr1, color clr2, uchar alpha = 255, int plot_shift = 0)
  {
   int start  = chart_first_vis_bar;
   int total  = chart_vis_bars + plot_shift;
   uint argb1 = ColorToARGB(clr1, alpha);
   uint argb2 = ColorToARGB(clr2, alpha);
   int limit  = fmin(ArraySize(serie1), ArraySize(serie2));
   int px, py1, py2;
   for(int i = 0; i < total; i++)
     {
      int bar_position = start - i;
      int bar_shift = start - i + plot_shift;
      int bar_index = limit - 1 - bar_shift;
      if(serie1[bar_index] == EMPTY_VALUE || serie1[bar_index] == EMPTY_VALUE || bar_shift >= limit)
         continue;
      int x  = ShiftToX(bar_position);
      int y1 = PriceToY(serie1[bar_index]);
      int y2 = PriceToY(serie2[bar_index]);
      uint argb = serie1[bar_index] < serie2[bar_index] ? argb2 : argb1;
      if(i > 0 && serie1[bar_index - 1] != EMPTY_VALUE && serie2[bar_index - 1] != EMPTY_VALUE)
        {
         if(py1 != py2)
            Canvas.FillTriangle(px, py1, px, py2, x, y1, argb);
         if(y1 != y2)
            Canvas.FillTriangle(px, py2, x, y1, x, y2, argb);
        }
      px  = x;
      py1 = y1;
      py2 = y2;
     }
  }

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

void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   chart_width          = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   chart_height         = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   chart_scale          = (int)ChartGetInteger(0, CHART_SCALE);
   chart_first_vis_bar  = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   chart_vis_bars       = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   chart_prcmin         = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   chart_prcmax         = ChartGetDouble(0, CHART_PRICE_MAX, 0);
   if(chart_width != Canvas.Width() || chart_height != Canvas.Height())
      Canvas.Resize(chart_width, chart_height);

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

  1. Обновим нашу функцию Redraw, удалив параметры, добавленные в предыдущем индикаторе, и добавив функцию DrawFilling.
  2. Добавим нашу функцию Redraw в OnCalculation, чтобы обновить рисунок при изменении значений индикатора.
  3. Изменим имя объекта, переданное в качестве параметра при вызове CreateBitmapLabel.

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = 0;
   color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0);
   color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   DrawFilling(ExtUpBuffer, ExtDownBuffer,clrup, clrdn, 128, InpMAShift);
//--- canvas update
   Canvas.Update();
  }
//--- the main loop of calculations
   for(int i=start; i<rates_total && !IsStopped(); i++)
     {
      ExtUpBuffer[i]=(1+InpDeviation/100.0)*ExtMABuffer[i];
      ExtDownBuffer[i]=(1-InpDeviation/100.0)*ExtMABuffer[i];
     }
   Redraw();
//--- OnCalculate done. Return new prev_calculated.
   return(rates_total);
   Canvas.CreateBitmapLabel(0, 0, short_name, 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE);

Теперь мы видим, как выглядит график с двумя конвертами с разными периодами и одним прямоугольным объектом.

Конверты с использованием CCanvas с альфа-каналом = 128

Конверты с использованием CCanvas с альфа-каналом = 255

Как видите, проблема с индикаторами решена, а проблема с объектами графика осталась, но это тема для другой главы.


Расширение метода для работы в индикаторах подокна

Рассмотрим рисунок, представленный ниже. Здесь мы видим индикатор подокна, использующий функцию DRAW_FILLING. Эта картинка была взята из документации по MQL. Мы сделаем то же самое, но при этом обеспечим прозрачность с помощью CCanvas и, что более важно, избежим проблем с областями перекрытия.

Индикатор подокна с использованием DRAW_FILLING

Необходимые изменения перечислены ниже:

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

//+------------------------------------------------------------------+
//| return the number of the subwindow where the indicator is located|
//+------------------------------------------------------------------+
int ChartIndicatorFind(string shortname)
  {
   int subwin = ChartGetInteger(0, CHART_WINDOWS_TOTAL);
   while(subwin > 0)
     {
      subwin--;
      int total = ChartIndicatorsTotal(0, subwin);
      for(int i = 0; i < total; i++)
        {
         string name = ChartIndicatorName(0, subwin, i);
         if(name == shortname)
            return subwin;
        }
     }
   return -1;
  }

В последнем индикаторе мы использовали в качестве примера индикатор Envelopes. Теперь будем использовать код из документации (DRAW_FILLING) в качестве источника для нашего примера. Мы можем создать новый индикатор в том же каталоге, в котором ранее создали два индикатора. Назовем его SubwindowIndicator. Затем скопируем код из документации.

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

#property indicator_plots   2
//--- plot Intersection
#property indicator_label1  "Fast"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label2  "Slow"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrBlue
#property indicator_width2  1

Изменения в функции OnInit.

//--- indicator buffers mapping
   SetIndexBuffer(0,IntersectionBuffer1,INDICATOR_DATA);
   SetIndexBuffer(1,IntersectionBuffer2,INDICATOR_DATA);
//---
   PlotIndexSetInteger(0,PLOT_SHIFT,InpMAShift);
   PlotIndexSetInteger(1,PLOT_SHIFT,InpMAShift);

Также нам не нужен индикатор для изменения вида линии. Мы можем закомментировать эту строку в функции OnCalculate.

//--- If a sufficient number of ticks has been accumulated
   if(ticks>=N)
     {
      //--- Change the line properties
      //ChangeLineAppearance();
      //--- Reset the counter of ticks to zero
      ticks=0;
     }

Теперь мы можем добавить переменные свойств графика и функции, созданные в этой статье. В этом индикаторе массивы, которые нам нужно передать в качестве параметров функции DrawFilling, имеют разные имена. Нам нужно изменить его в функции Redraw

double         IntersectionBuffer1[];
double         IntersectionBuffer2[];

Функция Redraw будет выглядеть так:

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = 0;
   color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0);
   color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   DrawFilling(IntersectionBuffer1, IntersectionBuffer2, clrup, clrdn, 128, InpMAShift);
//--- canvas update
   Canvas.Update();
  }

После компиляции кода получаем ожидаемый результат.

Индикатор подокна с прозрачным заполнением канала


Заключение

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

Файлы индикаторов, разработанных в статье, приложены ниже.