![Индикаторы на основе класса CCanvas: Заполнение каналов прозрачностью](https://c.mql5.com/2/52/filling-channels_600x314.jpg)
Индикаторы на основе класса CCanvas: Заполнение каналов прозрачностью
Введение
В этой статье мы рассмотрим методы создания пользовательских индикаторов, которые отрисовываются с помощью класса CCanvas Стандартной библиотеки. Особое внимание будет уделено индикаторам, заполняющим область между двумя линиями сплошным цветом. В первую очередь рассмотрим причины, по которым использование класса CCanvas может быть лучшим выбором по сравнению с другими вариантами для индикаторов такого типа. После этого взглянем на некоторые свойства графика, необходимые для расчета координат, и основной процесс, связанный с работой с CCanvas.
Конечная цель заключается в создании индикатора, применяющего прозрачность. Вся работа будет вестись только с учетом главного окна графика. Как только наша цель будет достигнута, мы можем начать работу с индикаторами подокна.
Темы статьи перечислены ниже:
- Причины использования класса CCanvas
- Свойства окна графика
- Понимание свойств окна графика
- Индикатор просмотра свойств графика
- Преобразование координат
- DRAW_FILLING с прозрачностью
- Расширение метода для работы в индикаторах подокна
Причины использования класса CCanvas
Кто-то может спросить, зачем использовать CCanvas, ведь для пользовательских индикаторов есть DRAW_FILLING. Есть как минимум две причины:
- Цвета индикатора смешиваются с цветами других индикаторов, свечей и объектов графика.
- DRAW_FILLING не допускает прозрачности
Свойства окна графика
Перед созданием пользовательского графика необходимо подумать о его свойствах. Вы можете найти все свойства в документации. Для получения значений свойств необходимо использовать соответствующие функции ChartGetInteger и ChartGetDouble. Также есть функция ChartGetString, но мы не будем здесь с ней работать.
Свойства, которые мы собираемся использовать, перечислены ниже с кратким описанием. Если нам понадобится что-то еще, я упомяну об этом отдельно.
- CHART_WIDTH_IN_PIXELS — ширина окна графика без учета ценовой шкалы
- CHART_HEIGTH _IN_PIXELS — высота подокна без учета шкалы дат
- CHART_PRICE_MIN — цена, соответствующая верхней части подокна
- CHART_PRICE_MAX — цена, соответствующая нижней части подокна
- CHART_SCALE — расстояние между барами. После некоторых тестов я обнаружил, что это степень двух pow(2, CHART_SCALE)
- CHART_FISRT_VISIBLE_BAR — первый видимый бар на графике слева направо
- CHART_VISIBLE_BARS — количество видимых баров на графике
Понимание свойств окна графика
Эти свойства хорошо видны на следующем рисунке.
Мы используем свойства 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). Тем временем мы выполняем основную отрисовку, которая состоит из стирания рисунков (очистка или установка значений пикселей по умолчанию для ресурса), создания рисунков и обновления ресурса. Полный жизненный цикл холста показан на диаграмме ниже.
Для простоты мы будем хранить 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);
Теперь мы сделаем несколько небольших обновлений, чтобы заставить функцию работать.
- Обновим нашу функцию Redraw, удалив параметры, добавленные в предыдущем индикаторе, и добавив функцию DrawFilling.
- Добавим нашу функцию Redraw в OnCalculation, чтобы обновить рисунок при изменении значений индикатора.
- Изменим имя объекта, переданное в качестве параметра при вызове 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);
Теперь мы видим, как выглядит график с двумя конвертами с разными периодами и одним прямоугольным объектом.
Как видите, проблема с индикаторами решена, а проблема с объектами графика осталась, но это тема для другой главы.
Расширение метода для работы в индикаторах подокна
Рассмотрим рисунок, представленный ниже. Здесь мы видим индикатор подокна, использующий функцию DRAW_FILLING. Эта картинка была взята из документации по MQL. Мы сделаем то же самое, но при этом обеспечим прозрачность с помощью CCanvas и, что более важно, избежим проблем с областями перекрытия.
Необходимые изменения перечислены ниже:
- Создание растровую метку в том же подокне, где расположен индикатор
- Изменение размера холста на основе размера подокна вместо основного окна графика
Чтобы создать растровую метку в том же подокне и получить размер подокна, нам нужно найти, в каком подокне находится индикатор. Можно подумать, что это просто последнее подокно графика, но терминал позволяет разместить два и более индикатора в одном подокне, причем необязательно в последнем. Также нам нужна функция, которая возвращает номер подокна, в котором находится индикатор. Взглянем на следующую функцию:
//+------------------------------------------------------------------+ //| 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, некоторые свойства графика и получение их значений, а также их использование для выполнения некоторых базовых преобразований координат, которые полезны и применимы для многих целей. После этого мы разработали индикатор с прозрачностью и расширили метод работы с индикаторами подокна.
Файлы индикаторов, разработанных в статье, приложены ниже.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/12357
![Биржевая сеточная торговля экспертом со стоповыми отложенными ордерами на Московской бирже (MOEX)](https://c.mql5.com/2/54/moex-mesh-trading-avatar.png)
![Управление капиталом в трейдинге](https://c.mql5.com/2/54/capital_control_avatar.png)
![Теория категорий в MQL5 (Часть 4): Интервалы, эксперименты и композиции](https://c.mql5.com/2/52/Category-Theory-p4-avatar.png)
![Алгоритм докупки: математическая модель увеличения эффективности](https://c.mql5.com/2/54/mathematical_model_to_increase_efficiency_Avatar.png)
![MQL5 - Язык торговых стратегий для клиентского терминала MetaTrader 5](https://c.mql5.com/i/registerlandings/logo-2.png)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Доброго времени!
А реально реализовать так чтобы, при прокрутке графика канва без задержки следовала за свечами как в случае с объектами?
Доброго времени!
А реально реализовать так чтобы, при прокрутке графика канва без задержки следовала за свечами как в случае с объектами?
можно поизгаляться через OBJ_BITMAP, когда левый верхний угол картинки привязывается к цена-время с перекрытием самого окна, т.е. когда размером канваса больше размеров самого окна.
В этом случае можно добиться почти полного отсутствия задержки.
Но там возникает проблема изменения вертикального масштаба и значительно усложняется событийная модель.
Поэтому лично бы я так не делал.
Есть еще альтернатива - отключать родной чарт и формировать свой собственный на канвасе. Но это годится, если вам не надо использовать другие индикаторы на этом чарте.