English
preview
Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики

Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики

MetaTrader 5Торговые системы |
73 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 12) мы расширили панель корреляционной матрицы в MetaQuotes Language 5 (MQL5) за счет интерактивных функций для повышения удобства использования. В части 13 мы представляем ценовую панель на основе холста с панелями графиков и статистики, предназначенную для предоставления трейдерам четкой и действенной информации о движении цен и показателях счета. Используя класс CCanvas, эта панель предлагает панели с возможностью перетаскивания и изменения размера, отображает недавнюю динамику цен и жизненно важную статистику, включая баланс, эквити и значения открытия/максимума/минимума/закрытия текущего бара. Она еще больше обогащает наши торговые решения за счет использования настраиваемых фонов, переключения тем оформления и обновлений в режиме реального времени. Эти инструменты позволяют более эффективно отслеживать изменения на рынке и реагировать на них. В статье рассмотрим следующие темы:

  1. Изучение структуры ценовой панели на основе холста
  2. Реализация средствами MQL5
  3. Тестирование на истории
  4. Заключение

В итоге у вас будет функциональная панель на основе холста в MQL5 для мониторинга цен и показателей, готовая к настройке. Перейдём к реализации!


Изучение структуры ценовой панели на основе холста

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

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

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

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

CANVAS DASHBOARD FRAMEWORK


Реализация средствами MQL5

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART1.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                                             //--- Declare canvas for graph
CCanvas canvasStats;                                             //--- Declare canvas for stats
CCanvas canvasHeader;                                            //--- Declare canvas for header

string canvasGraphName = "GraphCanvas";                          //--- Set graph canvas name
string canvasStatsName = "StatsCanvas";                          //--- Set stats canvas name
string canvasHeaderName = "HeaderCanvas";                        //--- Set header canvas name

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int graphBars                   = 50;                      // Number of recent bars to plot in the graph
input color borderColor               = clrBlack;                // Border color (change for white chart background)
input color borderHoverColor          = clrRed;                  // Border color on hover for resize indication

input int CanvasX                     = 30;                      // Main canvas X position
input int CanvasY                     = 50;                      // Main canvas Y position
input int CanvasWidth                 = 400;                     // Main canvas width
input int CanvasHeight                = 300;                     // Main canvas height

input bool EnableStatsPanel           = true;                    // Enable second stats panel
input int PanelGap                    = 10;                      // Gap between panels in pixels

input bool UseBackground              = true;                    // Enable background image
input double FogOpacity               = 0.5;                     // Fog opacity (0.0 = no fog/fully transparent, 1.0 = fully opaque)
input bool BlendFog                   = true;                    // Blend fog with image (true: image visible under fog; false: original fog hides image)

input int StatsFontSize               = 12;                      // Font size for stats panel text
input color StatsLabelColor           = clrDodgerBlue;           // Color for stats labels
input color StatsValueColor           = clrWhite;                // Color for stats values
input color StatsHeaderColor          = clrDodgerBlue;           // Color for stats headers
input int StatsHeaderFontSize         = 14;                      // Font size for stats headers

input double BorderOpacityPercentReduction = 20.0;               // Percent reduction for border opacity (0-100)
input double BorderDarkenPercent      = 30.0;                    // Percent to darken borders (0-100)

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_BACKGROUND_MODE
{
   NoColor          = 0,         // No color fill
   SingleColor      = 1,         // Single color fill
   GradientTwoColors = 2         // Gradient with two colors
};

input ENUM_BACKGROUND_MODE StatsBackgroundMode = GradientTwoColors; // Stats background mode
input color TopColor                   = clrBlack;                  // Top color for gradient or single fill
input color BottomColor                = clrRed;                    // Bottom color for gradient
input double BackgroundOpacity         = 0.7;                       // Opacity for stats background fill (0.0 to 1.0)

enum ENUM_RESIZE_MODE
{
   NONE,
   BOTTOM,
   RIGHT,
   BOTTOM_RIGHT
};

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "1. Transparent MT5 bmp image.bmp"                     // Hardcoded background image resource

//+------------------------------------------------------------------+
//| Global Variables Continued                                       |
//+------------------------------------------------------------------+
uint original_bg_pixels[];                                       //--- Declare original unscaled background
uint orig_w = 0, orig_h = 0;                                     //--- Initialize original dimensions
uint bg_pixels_graph[];                                          //--- Declare scaled background for graph
uint bg_pixels_stats[];                                          //--- Declare scaled background for stats

int currentCanvasX = CanvasX;                                    //--- Set current X position
int currentCanvasY = CanvasY;                                    //--- Set current Y position
int currentWidth = CanvasWidth;                                  //--- Set current width
int currentHeight = CanvasHeight;                                //--- Set current height
bool panel_dragging = false;                                     //--- Set dragging flag
int panel_drag_x = 0, panel_drag_y = 0;                          //--- Initialize drag start mouse coordinates
int panel_start_x = 0, panel_start_y = 0;                        //--- Initialize drag start panel coordinates
bool resizing = false;                                           //--- Set resizing flag
ENUM_RESIZE_MODE resize_mode = NONE;                             //--- Set resize mode
ENUM_RESIZE_MODE hover_mode = NONE;                              //--- Set hover mode for resize
int resize_start_x = 0, resize_start_y = 0;                      //--- Initialize resize start coordinates
int start_width = 0, start_height = 0;                           //--- Initialize start dimensions
const int resize_thickness = 5;                                  //--- Set resize border thickness
const int min_width = 200;                                       //--- Set minimum width
const int min_height = 150;                                      //--- Set minimum height
int hover_mouse_local_x = 0;                                     //--- Set local mouse x for icon
int hover_mouse_local_y = 0;                                     //--- Set local mouse y for icon
bool header_hovered = false;                                     //--- Set header hover flag
bool minimize_hovered = false;                                   //--- Set minimize hover flag
bool close_hovered = false;                                      //--- Set close hover flag
bool theme_hovered = false;                                      //--- Set theme hover flag
bool resize_hovered = false;                                     //--- Set resize hover flag
int prev_mouse_state = 0;                                        //--- Initialize previous mouse state
int last_mouse_x = 0, last_mouse_y = 0;                          //--- Initialize last mouse position
int header_height = 27;                                          //--- Set header height
int gap_y = 7;                                                   //--- Set y gap
int button_size = 25;                                            //--- Set button size
int theme_x_offset = -75;                                        //--- Set theme offset relative to header right
int minimize_x_offset = -50;                                     //--- Set minimize offset relative to header right
int close_x_offset = -25;                                        //--- Set close offset relative to header right
bool panels_minimized = false;                                   //--- Set minimized flag
color HeaderColor = C'60,60,60';                                 //--- Set header color
color HeaderHoverColor = clrRed;                                 //--- Set header hover color
color HeaderDragColor = clrMediumBlue;                           //--- Set header drag color

bool is_dark_theme = true;                                       //--- Set dark theme flag

color LightHeaderColor = clrSilver;                              //--- Set light header color
color LightHeaderTextColor = clrBlack;                           //--- Set light header text color
color LightStatsLabelColor = clrBlue;                            //--- Set light stats label color
color LightStatsValueColor = clrBlack;                           //--- Set light stats value color
color LightStatsHeaderColor = clrBlue;                           //--- Set light stats header color
color LightBorderColor = clrBlack;                               //--- Set light border color
color LightTopColor = clrGreen;                                  //--- Set light top color
color LightBottomColor = clrGold;                                //--- Set light bottom color
color LightHeaderHoverColor = clrRed;                            //--- Set light header hover color
color LightHeaderDragColor = clrMediumBlue;                      //--- Set light header drag color

bool graphCreated = false;                                       //--- Set graph created flag
bool statsCreated = false;                                       //--- Set stats created flag

Мы начинаем реализацию с включения библиотеки canvas с помощью "#include <Canvas/Canvas.mqh>", которая предоставляет класс CCanvas для создания графических панелей на основе растровых изображений на графике и управления ими. Затем объявляем три объекта "CCanvas": "canvasGraph" для панели графика цен, "canvasStats" для панели статистики и "canvasHeader" для раздела заголовков. Мы задаем строковые константы для их имен как "GraphCanvas", "StatsCanvas" и "HeaderCanvas", чтобы уникально идентифицировать их.

Затем определяем входные параметры для настройки панели: "graphBars" - пятьдесят для количества отображаемых баров, "borderColor" - черный и "borderHoverColor" - красный для рамок и указаний при наведении курсора, положения и размеры, такие как "CanvasX" - тридцать, "CanvasY" - пятьдесят, "CanvasWidth" - четыреста, "CanvasHeight" - триста, логическое значение "EnableStatsPanel" true для отображения панели статистики с "PanelGap" на десять пикселей, "UseBackground" - true для фона изображения с "FogOpacity" на 0,5 и "BlendFog" - true для смешивания, размеров шрифта и цветов для статистики, такой как "StatsFontSize" - в двенадцать, "StatsLabelColor" на яркий оттенок лазурного цвета (dodger blue) и процентные значения для корректировки рамок, такие как "BorderOpacityPercentReduction" в 20,0 и "BorderDarkenPercent" в 30,0.

Мы создаём перечисление "ENUM_BACKGROUND_MODE" с параметрами "NoColor" для отсутствия заливки, "SingleColor" для однородного цвета и "GradientTwoColors" для двухцветных градиентов, при этом входной признак "StatsBackgroundMode" по умолчанию имеет значение градиента, а цвета "TopColor" — на чёрный, "BottomColor" — на красный, плюс "BackgroundOpacity" равное 0,7. Затем добавляем перечисление "ENUM_RESIZE_MODE" для изменения размеров состояний: "NONE", "BOTTOM", "RIGHT" и "BOTTOM_RIGHT".

Включаем ресурс с помощью директивы #resource; "1. Transparent MetaTrader 5 bmp image.bmp" для жестко заданного фонового изображения. Изображение, которое вы прикрепляете, должно быть только в виде растрового файла. Это можно легко это сделать, и наш файл изображения теперь выглядит следующим образом. Для простоты мы импортировали его туда, где находится файл программы. Посмотрим на то, что у нас получилось.

BITMAP IMAGE FILE

После подготовки файла изображения мы продолжаем с глобальными переменными: массивы "original_bg_pixels", "bg_pixels_graph" и "bg_pixels_stats" для пикселей изображения, размеры "orig_w" и "orig_h" равны нулю, текущие позиции и размеры инициализируются из входных признаков, логические значения, такие как "panel_dragging" - в значении false, "resizing" - в значении false, перечисления "resize_mode" и "hover_mode" равны "NONE", целые числа для начала и минимума изменения размера, такие как "resize_thickness" равны пяти, "min_width" - равно двумстам, флаги наведения курсора, такие как "header_hovered" - в значении false, "prev_mouse_state" равно нулю, константы структуры панели, такие как "header_height" равны двадцати семи, "gap_y" равно семи, "button_size" равно двадцати пяти, смещения для кнопок, "panels_minimized" - в значении false, цвета, такие как "HeaderColor" - в средне-серый (medium gray), "HeaderHoverColor" установлен в красный цвет, флаг темы "is_dark_theme" — в true, цвета светлого режима, такие как "LightHeaderColor", — в серебристый, а флаги "graphCreated" и "statsCreated" — в значение false.

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

//+------------------------------------------------------------------+
//| Scale image                                                      |
//+------------------------------------------------------------------+
void ScaleImage(uint &pixels[], int original_width, int original_height, int new_width, int new_height) {
   uint scaled_pixels[];                                         //--- Declare scaled array
   ArrayResize(scaled_pixels, new_width * new_height);           //--- Resize scaled
   for (int y = 0; y < new_height; y++)                          //--- Loop new rows
   {
      for (int x = 0; x < new_width; x++)                        //--- Loop new columns
      {
         double original_x = (double)x * original_width / new_width; //--- Compute original x
         double original_y = (double)y * original_height / new_height; //--- Compute original y
         uint pixel = BicubicInterpolate(pixels, original_width, original_height, original_x, original_y); //--- Interpolate pixel
         scaled_pixels[y * new_width + x] = pixel;               //--- Set scaled pixel
      }
   }
   ArrayResize(pixels, new_width * new_height);                  //--- Resize original
   ArrayCopy(pixels, scaled_pixels);                             //--- Copy scaled
}

//+------------------------------------------------------------------+
//| Bicubic interpolate pixel                                        |
//+------------------------------------------------------------------+
uint BicubicInterpolate(uint &pixels[], int width, int height, double x, double y) {
   int x0 = (int)x;                                              //--- Get integer x
   int y0 = (int)y;                                              //--- Get integer y
   double fractional_x = x - x0;                                 //--- Get fractional x
   double fractional_y = y - y0;                                 //--- Get fractional y
   int x_indices[4], y_indices[4];                               //--- Declare indices
   for (int i = -1; i <= 2; i++)                                 //--- Loop offsets
   {
      x_indices[i + 1] = MathMin(MathMax(x0 + i, 0), width - 1); //--- Clamp x index
      y_indices[i + 1] = MathMin(MathMax(y0 + i, 0), height - 1); //--- Clamp y index
   }
   uint neighborhood_pixels[16];                                 //--- Declare neighborhood
   for (int j = 0; j < 4; j++)                                   //--- Loop y indices
   {
      for (int i = 0; i < 4; i++)                                //--- Loop x indices
      {
         neighborhood_pixels[j * 4 + i] = pixels[y_indices[j] * width + x_indices[i]]; //--- Get pixel
      }
   }
   uchar alpha_components[16], red_components[16], green_components[16], blue_components[16]; //--- Declare components
   for (int i = 0; i < 16; i++)                                  //--- Loop pixels
   {
      GetArgb(neighborhood_pixels[i], alpha_components[i], red_components[i], green_components[i], blue_components[i]); //--- Get ARGB
   }
   uchar alpha_out = (uchar)BicubicInterpolateComponent(alpha_components, fractional_x, fractional_y); //--- Interpolate alpha
   uchar red_out = (uchar)BicubicInterpolateComponent(red_components, fractional_x, fractional_y); //--- Interpolate red
   uchar green_out = (uchar)BicubicInterpolateComponent(green_components, fractional_x, fractional_y); //--- Interpolate green
   uchar blue_out = (uchar)BicubicInterpolateComponent(blue_components, fractional_x, fractional_y); //--- Interpolate blue
   return (((uint)alpha_out) << 24) | (((uint)red_out) << 16) | (((uint)green_out) << 8) | ((uint)blue_out); //--- Return interpolated
}

//+------------------------------------------------------------------+
//| Bicubic interpolate component                                    |
//+------------------------------------------------------------------+
double BicubicInterpolateComponent(uchar &components[], double fractional_x, double fractional_y) {
   double weights_x[4];                                          //--- Declare x weights
   double t = fractional_x;                                      //--- Set t x
   weights_x[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_x[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_x[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_x[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double y_values[4];                                           //--- Declare y values
   for (int j = 0; j < 4; j++)                                   //--- Loop y
   {
      y_values[j] = weights_x[0] * components[j * 4 + 0] + weights_x[1] * components[j * 4 + 1] + weights_x[2] * components[j * 4 + 2] + weights_x[3] * components[j * 4 + 3]; //--- Compute y value
   }
   double weights_y[4];                                          //--- Declare y weights
   t = fractional_y;                                             //--- Set t y
   weights_y[0] = (-0.5 * t * t * t + t * t - 0.5 * t);          //--- Compute weight 0
   weights_y[1] = (1.5 * t * t * t - 2.5 * t * t + 1);           //--- Compute weight 1
   weights_y[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);      //--- Compute weight 2
   weights_y[3] = (0.5 * t * t * t - 0.5 * t * t);               //--- Compute weight 3
   double result = weights_y[0] * y_values[0] + weights_y[1] * y_values[1] + weights_y[2] * y_values[2] + weights_y[3] * y_values[3]; //--- Compute result
   return MathMax(0, MathMin(255, result));                      //--- Clamp result
}

//+------------------------------------------------------------------+
//| Get ARGB components                                              |
//+------------------------------------------------------------------+
void GetArgb(uint pixel, uchar &alpha, uchar &red, uchar &green, uchar &blue) {
   alpha = (uchar)((pixel >> 24) & 0xFF);                        //--- Get alpha
   red = (uchar)((pixel >> 16) & 0xFF);                          //--- Get red
   green = (uchar)((pixel >> 8) & 0xFF);                         //--- Get green
   blue = (uchar)(pixel & 0xFF);                                 //--- Get blue
}

Сначала мы реализуем функцию "ScaleImage" для изменения размера массива изображений в пикселях с исходных размеров на новые ширину и высоту, используя бикубическую интерполяцию для плавного масштабирования. Она содержит ссылку на массив пикселей, исходную ширину и высоту, а также новые размеры. Мы объявляем и изменяем размер временного массива "scaled_pixels" до нового размера, затем вкладываем циклы по новой высоте и ширине, чтобы вычислить соответствующие исходные координаты с помощью пропорционального картирования. Для каждого нового пикселя вызываем "BicubicInterpolate" с исходным массивом и дробными координатами, чтобы получить интерполированное значение, и сохраняем его в переменной "scaled_pixels". Наконец, мы изменяем размер входного массива пикселей до нового размера и копируем в него "scaled_pixels" с помощью функции ArrayCopy

Далее следует функция "BicubicInterpolate" для вычисления значения в одном пикселе с использованием бикубической интерполяции при заданных дробных значениях x и y на изображении. Она принимает массив пикселей, ширину, высоту и удваивает значения x и y. Мы получаем целые части x0 и y0, дробные части и создаем индексные массивы для окрестности 4x4, привязывая смещения от -1 до 2 к границам изображения с помощью функций MathMin и MathMax. Мы выделяем шестнадцать соседних пикселей в массив, затем объявляем массивы компонентов для альфа-, красного, зеленого и синего цветов и заполняем их циклически, используя "GetArgb". Мы интерполируем каждый компонент с помощью "BicubicInterpolateComponent" и дробей, преобразуя их в uchar, и объединяем в значение uint ARGB с битовыми сдвигами.

Затем создаём функцию "BicubicInterpolateComponent" для выполнения бикубической интерполяции в массиве компонентов 4x4 одного цветового канала с использованием дробных значений x и y. Она вычисляет четыре весовых коэффициента x с помощью формулы бикубического ядра, основываясь на t как дробном значении x, а затем вычисляет четыре промежуточных значения y путем взвешенных сумм строк в массиве компонентов. Аналогичным образом, она вычисляет весовые коэффициенты y, используя дробную часть y в качестве t, и выводит окончательный результат как взвешенную сумму этих значений y, ограничивая диапазон от нуля до 255 с помощью параметров "MathMax" и "MathMin". Наконец, мы реализуем функцию "GetArgb" для извлечения компонентов ARGB из пиксельного значения uint в числовые значения (uchar) для альфа-, красного, зеленого и синего цветов, используя битовые сдвиги вправо на 24/16/8/0 и маскирование с помощью 0xFF.

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

DIFFERENT INTERPOLATION APPROACHES

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

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()
{
   currentWidth = CanvasWidth;                                   //--- Set initial width
   currentHeight = CanvasHeight;                                 //--- Set initial height
   currentCanvasX = CanvasX;                                     //--- Set initial X
   currentCanvasY = CanvasY;                                     //--- Set initial Y
   
   if (UseBackground)                                            //--- Check if background enabled
   {
      if (ResourceReadImage("::1. Transparent MT5 bmp image.bmp", original_bg_pixels, orig_w, orig_h) && orig_w > 0 && orig_h > 0) //--- Load image if valid
      {
         ArrayCopy(bg_pixels_graph, original_bg_pixels);         //--- Copy to graph background
         ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph background
         if (EnableStatsPanel)                                   //--- Check stats panel
         {
            ArrayCopy(bg_pixels_stats, original_bg_pixels);      //--- Copy to stats background
            ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats background
         }
      }
      else                                                       //--- Handle load failure
      {
         Print("Failed to load background image from ::1. Transparent MT5 bmp image.bmp"); //--- Print error
      }
   }
   
   int header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute header width
   if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX, currentCanvasY, header_width, header_height, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create header canvas
   {
      Print("Failed to create Header Canvas");                   //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   
   if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create graph canvas
   {
      Print("Failed to create Graph Canvas");                    //--- Print error
      return(INIT_FAILED);                                       //--- Return failure
   }
   graphCreated = true;                                          //--- Set graph created
   
   if (EnableStatsPanel)                                         //--- Check stats panel
   {
      int statsX = currentCanvasX + currentWidth + PanelGap;     //--- Compute stats X
      if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Create stats canvas
      {
         Print("Failed to create Stats Canvas");                 //--- Print error
      }
      statsCreated = true;                                       //--- Set stats created
   }
   
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

В обработчике OnInit мы настраиваем начальное состояние и создаем панели холста для дашборда. Мы инициализируем текущие размеры и положения, из входных признаков, таких как "currentWidth" в "CanvasWidth", "currentHeight" в "CanvasHeight", "currentCanvasX" в "CanvasX", а также из "currentCanvasY" в "CanvasY". Если параметр "UseBackground" имеет значение true, мы загружаем исходное изображение с помощью функции ResourceReadImage в переменную "original_bg_pixels" и получаем его исходную ширину и высоту. В случае успеха копируем изображение в "bg_pixels_graph" и масштабируем его до текущих размеров с помощью функции "ScaleImage". Для панели статистики, если параметр "EnableStatsPanel" имеет значение true, мы копируем данные в "bg_pixels_stats" и масштабируем до половины ширины, сохраняя при этом полную высоту. В случае сбоя выводим сообщение об ошибке.

Мы вычисляем ширину заголовка как ширину графика плюс ширину опциональной статистики и разрыв, затем создаем холст заголовка с помощью "CreateBitmapLabel" в нулевом подокне, с именем "canvasHeaderName", позицией, шириной и "header_height", используя COLOR_FORMAT_ARGB_NORMALIZE, выводя сообщение об ошибке и возвращая "INIT_FAILED" в случае неудачи. Аналогично, создаем графический холст под заголовком с добавлением "gap_y", устанавливая для "graphCreated" значение true в случае успеха или возвращая INIT_FAILED в обратном случае. Если статистика включена, вычисляем положение x после графика плюс "PanelGap", создаем холст статистики и устанавливаем для "statsCreated" значение true. Наконец, перерисовываем график с помощью функции "ChartRedraw" и возвращаем INIT_SUCCEEDED. Это лишь создаёт области отрисовки, но сами элементы ещё не нарисованы. Чтобы продемонстрировать это, вот образец того, что мы можем увидеть во всплывающих подсказках при компиляции.

CANVAS AREAS

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

//+------------------------------------------------------------------+
//| Get theme-aware colors                                           |
//+------------------------------------------------------------------+
color GetHeaderColor() { return is_dark_theme ? HeaderColor : LightHeaderColor; }                //--- Return header color
color GetHeaderHoverColor() { return is_dark_theme ? HeaderHoverColor : LightHeaderHoverColor; } //--- Return hover color
color GetHeaderDragColor() { return is_dark_theme ? HeaderDragColor : LightHeaderDragColor; }    //--- Return drag color
color GetStatsLabelColor() { return is_dark_theme ? StatsLabelColor : LightStatsLabelColor; }    //--- Return label color
color GetStatsValueColor() { return is_dark_theme ? StatsValueColor : LightStatsValueColor; }    //--- Return value color
color GetStatsHeaderColor() { return is_dark_theme ? StatsHeaderColor : LightStatsHeaderColor; } //--- Return header color
color GetBorderColor() { return is_dark_theme ? borderColor : LightBorderColor; }                //--- Return border color
color GetTopColor() { return is_dark_theme ? TopColor : LightTopColor; }                         //--- Return top color
color GetBottomColor() { return is_dark_theme ? BottomColor : LightBottomColor; }                //--- Return bottom color
color GetHeaderTextColor() { return is_dark_theme ? clrWhite : LightHeaderTextColor; }           //--- Return text color
color GetIconColor(bool is_drag) { return is_drag ? GetHeaderDragColor() : GetHeaderHoverColor(); } //--- Return icon color

Здесь мы реализуем несколько функций методов-получателей для получения цветов, соответствующих теме оформления, на основе текущего флага "is_dark_theme", обеспечивая последовательные визуальные элементы в темном и светлом режимах без избыточных проверок в других местах. Функция "GetHeaderColor" возвращает значение "HeaderColor" в темном режиме или "LightHeaderColor" в светлом режиме для фона заголовка. Для всех остальных функций мы используем ту же логику. Теперь мы можем использовать эти вспомогательные функции для создания объектов холст заголовка.

//+------------------------------------------------------------------+
//| Draw header on header canvas                                     |
//+------------------------------------------------------------------+
void DrawHeaderOnCanvas()
{
   canvasHeader.Erase(0);                                        //--- Clear canvas
   
   color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Set background
   uint argb_bg = ColorToARGB(header_bg, 255);                   //--- Convert to ARGB
   canvasHeader.FillRectangle(0, 0, canvasHeader.Width() - 1, header_height - 1, argb_bg); //--- Fill background
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasHeader.Line(0, 0, canvasHeader.Width() - 1, 0, argbBorder); //--- Draw top border
   canvasHeader.Line(canvasHeader.Width() - 1, 0, canvasHeader.Width() - 1, header_height - 1, argbBorder); //--- Draw right border
   canvasHeader.Line(canvasHeader.Width() - 1, header_height - 1, 0, header_height - 1, argbBorder); //--- Draw bottom border
   canvasHeader.Line(0, header_height - 1, 0, 0, argbBorder);    //--- Draw left border
   
   canvasHeader.FontSet("Arial Bold", 15);                       //--- Set font
   uint argbText = ColorToARGB(GetHeaderTextColor(), 255);       //--- Convert text to ARGB
   canvasHeader.TextOut(10, (header_height - 15) / 2, "Price Dashboard", argbText, TA_LEFT); //--- Draw title
   
   int theme_x = canvasHeader.Width() + theme_x_offset;          //--- Compute theme x
   string theme_symbol = CharToString((uchar)91);                //--- Set theme symbol
   color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Set theme color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_theme = ColorToARGB(theme_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(theme_x, (header_height - 22) / 2, theme_symbol, argb_theme, TA_CENTER); //--- Draw theme icon
   
   int min_x = canvasHeader.Width() + minimize_x_offset;         //--- Compute minimize x
   string min_symbol = panels_minimized ? CharToString((uchar)111) : CharToString((uchar)114); //--- Set minimize symbol
   color min_color = minimize_hovered ? clrYellow : GetHeaderTextColor(); //--- Set minimize color
   canvasHeader.FontSet("Wingdings", 22);                        //--- Set font
   uint argb_min = ColorToARGB(min_color, 255);                  //--- Convert to ARGB
   canvasHeader.TextOut(min_x, (header_height - 22) / 2, min_symbol, argb_min, TA_CENTER); //--- Draw minimize icon
   
   int close_x = canvasHeader.Width() + close_x_offset;          //--- Compute close x
   string close_symbol = CharToString((uchar)114);               //--- Set close symbol
   color close_color = close_hovered ? clrRed : GetHeaderTextColor(); //--- Set close color
   canvasHeader.FontSet("Webdings", 22);                         //--- Set font
   uint argb_close = ColorToARGB(close_color, 255);              //--- Convert to ARGB
   canvasHeader.TextOut(close_x, (header_height - 22) / 2, close_symbol, argb_close, TA_CENTER); //--- Draw close icon
   
   canvasHeader.Update();                                        //--- Update canvas
}

Здесь мы реализуем функцию "DrawHeaderOnCanvas" для отображения раздела заголовка на объекте "canvasHeader", предоставляя заголовок и интерактивные значки с динамическими цветами на основе таких состояний, как перетаскивание или наведение курсора. Начнём с очистки холста методом "Erase", установив значение «ноль». Цвет фона определяется условно: если "panel_dragging" равно true, используется "GetHeaderDragColor"; в противном случае, если "header_hovered", используется "GetHeaderHoverColor"; в противном случае — "GetHeaderColor". Преобразуем его в ARGB с помощью ColorToARGB с полной непрозрачностью 255 и заполняем прямоугольник от (0,0) шириной минус один и параметром "header_height" минус один, используя метод FillRectangle

Для рамок преобразуем цвет рамки из функции "GetBorderColor" в значение ARGB 255 и рисуем линии с помощью функции Line для верхнего, правого, нижнего и левого краев заголовка. Мы устанавливаем шрифт Arial Bold размером пятнадцать с помощью FontSet, преобразуем цвет текста заголовка из "GetHeaderTextColor" в ARGB и рисуем заголовок 'Price Dashboard' в координате по оси x десять, по центру вертикально с помощью TextOut и выравниванием по левому краю.

Для значка темы мы вычисляем его координату по оси x как ширину холста плюс "theme_x_offset", устанавливаем символ в знак 91 из преобразования uchar, цвет в желтый, если "theme_hovered", в противном случае цвет текста заголовка, меняем шрифт на Wingdings размером двадцать два, преобразуем в ARGB и рисуем по центру по горизонтали и вертикали с помощью функции "TextOut" и выравниваем по центру. Аналогично, для значка сворачивания вычисляем x с помощью "minimize_x_offset", устанавливаем условное значение символа как 111, если "panels_minimized", или 114 в противном случае, изменяем цвет на желтый при наведении курсора, в противном случае цвет текста, используем шрифт Wingdings, преобразуем и отрисовываем по центру. Для значка закрытия вычисляем x с помощью "close_x_offset", устанавливаем для символа значение 114, красный цвет при "close_hovered", в противном случае - цвет текста, переключаем шрифт на Webdings и размер двадцать два, преобразовываем и отрисовываем по центру. Наконец, для отображения изменений обновляем холст с помощью "Update". При вызове функции во время инициализации получаем следующий результат.

CANVAS HEADER RENDER

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

//+------------------------------------------------------------------+
//| Update the price graph on the main Canvas                        |
//+------------------------------------------------------------------+
void UpdateGraphOnCanvas()
{
   canvasGraph.Erase(0);                                         //--- Clear canvas
   
   if (UseBackground && ArraySize(bg_pixels_graph) == currentWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < currentWidth; x++)                  //--- Loop columns
         {
            canvasGraph.PixelSet(x, y, bg_pixels_graph[y * currentWidth + x]); //--- Set pixel
         }
      }
   }
   
   uint argbBorder = ColorToARGB(GetBorderColor(), 255);         //--- Convert border to ARGB
   canvasGraph.Line(0, 0, currentWidth - 1, 0, argbBorder);      //--- Draw top outer
   canvasGraph.Line(currentWidth - 1, 0, currentWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
   canvasGraph.Line(currentWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
   canvasGraph.Line(0, currentHeight - 1, 0, 0, argbBorder);     //--- Draw left outer
   canvasGraph.Line(1, 1, currentWidth - 2, 1, argbBorder);      //--- Draw top inner
   canvasGraph.Line(currentWidth - 2, 1, currentWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
   canvasGraph.Line(currentWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
   canvasGraph.Line(1, currentHeight - 2, 1, 1, argbBorder);     //--- Draw left inner
   
   double closePrices[];                                         //--- Declare close array
   ArrayResize(closePrices, graphBars);                          //--- Resize close
   if (CopyClose(_Symbol, _Period, 0, graphBars, closePrices) != graphBars) //--- Copy closes
   {
      Print("Failed to copy close prices");                      //--- Print error
      return;                                                    //--- Exit
   }
   
   datetime timeArr[];                                           //--- Declare time array
   ArrayResize(timeArr, graphBars);                              //--- Resize time
   if (CopyTime(_Symbol, _Period, 0, graphBars, timeArr) != graphBars) //--- Copy times
   {
      Print("Failed to copy times");                             //--- Print error
      return;                                                    //--- Exit
   }
   
   double minPrice = closePrices[0];                             //--- Set initial min
   double maxPrice = closePrices[0];                             //--- Set initial max
   for (int i = 1; i < graphBars; i++)                           //--- Loop prices
   {
      if (closePrices[i] < minPrice) minPrice = closePrices[i];  //--- Update min
      if (closePrices[i] > maxPrice) maxPrice = closePrices[i];  //--- Update max
   }
   double priceRange = maxPrice - minPrice;                      //--- Compute range
   if (priceRange == 0) priceRange = _Point;                     //--- Avoid zero
   
   int graphLeft = 2;                                            //--- Set left margin
   int graphRight = currentWidth - 3;                            //--- Set right margin
   double graphWidth_d = graphRight - graphLeft;                 //--- Compute width
   int graphHeight = currentHeight - 4;                          //--- Compute height
   int bottomY = 2 + graphHeight;                                //--- Set bottom y
   
   int x_pos[];                                                  //--- Declare x positions
   int y_pos[];                                                  //--- Declare y positions
   ArrayResize(x_pos, graphBars);                                //--- Resize x
   ArrayResize(y_pos, graphBars);                                //--- Resize y
   for (int i = 0; i < graphBars; i++)                           //--- Loop bars
   {
      double norm = (graphBars > 1) ? (double)i / (graphBars - 1) : 0.0; //--- Normalize
      x_pos[i] = graphLeft + (int)(norm * graphWidth_d + 0.5);   //--- Set x
      double price = closePrices[graphBars - 1 - i];             //--- Get price (flipped)
      y_pos[i] = 2 + (int)(graphHeight * (maxPrice - price) / priceRange + 0.5); //--- Set y
   }
   
   color lineColor = clrBlue;                                    //--- Set line color
   uint argbLine = ColorToARGB(lineColor, 255);                  //--- Convert to ARGB
   for (int i = 0; i < graphBars - 1; i++)                       //--- Loop segments
   {
      int x1 = (currentWidth - 1) - x_pos[i];                    //--- Set x1 (flipped)
      int y1 = y_pos[i];                                         //--- Set y1
      int x2 = (currentWidth - 1) - x_pos[i + 1];                //--- Set x2 (flipped)
      int y2 = y_pos[i + 1];                                     //--- Set y2
      canvasGraph.LineAA(x1, y1, x2, y2, argbLine);              //--- Draw line
   }
   
   int min_flipped_x = (currentWidth - 1) - graphRight;          //--- Set min flipped x
   int max_flipped_x = (currentWidth - 1) - graphLeft;           //--- Set max flipped x
   for (int colX = min_flipped_x; colX <= max_flipped_x; colX++) //--- Loop columns
   {
      int logical_colX = (currentWidth - 1) - colX;              //--- Get logical x
      int seg = -1;                                              //--- Initialize segment
      for (int j = 0; j < graphBars - 1; j++)                    //--- Loop segments
      {
         if (x_pos[j] <= logical_colX && logical_colX <= x_pos[j + 1]) //--- Check segment
         {
            seg = j;                                             //--- Set segment
            break;                                               //--- Exit
         }
      }
      if (seg == -1) continue;                                   //--- Skip if no segment
      
      double dx = x_pos[seg + 1] - x_pos[seg];                   //--- Compute dx
      double t = (dx > 0) ? (logical_colX - x_pos[seg]) / dx : 0.0; //--- Compute t
      double interpY = y_pos[seg] + t * (y_pos[seg + 1] - y_pos[seg]); //--- Interpolate y
      int topY = (int)(interpY + 0.5);                           //--- Round top y
      
      for (int fillY = topY; fillY < bottomY; fillY++)           //--- Loop fill
      {
         double fadeFactor = (double)(bottomY - fillY) / (bottomY - topY); //--- Compute fade
         uchar alpha = (uchar)(255 * fadeFactor * FogOpacity);   //--- Compute alpha
         uint argbFill = ColorToARGB(lineColor, alpha);          //--- Convert fill
         if (BlendFog)                                           //--- Check blend
         {
            uint currentPixel = canvasGraph.PixelGet(colX, fillY); //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels
            canvasGraph.PixelSet(colX, fillY, blendedPixel);     //--- Set blended
         }
         else                                                    //--- Handle no blend
         {
            canvasGraph.PixelSet(colX, fillY, argbFill);         //--- Set fill
         }
      }
   }
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   uint argbText = ColorToARGB(is_dark_theme ? clrBlack : clrGray, 255); //--- Convert text
   canvasGraph.TextOut(currentWidth / 2, 10, "Price Graph (" + _Symbol + ")", argbText, TA_CENTER); //--- Draw title
   
   canvasGraph.FontSet("Arial", 12);                             //--- Set font
   
   string newTime = TimeToString(timeArr[0], TIME_DATE | TIME_MINUTES); //--- Get new time
   string oldTime = TimeToString(timeArr[graphBars - 1], TIME_DATE | TIME_MINUTES); //--- Get old time
   canvasGraph.TextOut(10, currentHeight - 15, newTime, argbText, TA_LEFT); //--- Draw new time
   canvasGraph.TextOut(currentWidth - 10, currentHeight - 15, oldTime, argbText, TA_RIGHT); //--- Draw old time
   
   if (resize_hovered || resizing)                               //--- Check resize state
   {
      ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get active mode
      if (active_mode == NONE)                                       //--- Check none
      {
         canvasGraph.Update();                                   //--- Update canvas
         return;                                                 //--- Exit
      }
      
      string icon_font = "Wingdings 3";                          //--- Set icon font
      int icon_size = 25;                                        //--- Set icon size
      uchar icon_code;                                           //--- Declare code
      int angle = 0;                                             //--- Set angle
      switch (active_mode)                                       //--- Switch mode
      {
         case BOTTOM: 
            icon_code = (uchar)'2';                              //--- Set bottom code
            angle = 0;                                           //--- Set angle
            break;
         case RIGHT: 
            icon_code = (uchar)'1';                              //--- Set right code
            angle = 0;                                           //--- Set angle
            break;
         case BOTTOM_RIGHT: 
            icon_code = (uchar)'2';                              //--- Set corner code
            angle = 450;                                         //--- Set angle
            break;
         default: canvasGraph.Update(); return;
      }
      string icon_symbol = CharToString(icon_code);              //--- Set symbol
      
      color icon_color = GetIconColor(resizing);                 //--- Get icon color
      uint argb_icon = ColorToARGB(icon_color, 255);             //--- Convert to ARGB
      
      canvasGraph.FontSet(icon_font, icon_size);                 //--- Set font
      canvasGraph.FontAngleSet(angle);                           //--- Set angle
      
      int icon_x = 0;                                            //--- Initialize x
      int icon_y = 0;                                            //--- Initialize y
      
      switch (active_mode)                                       //--- Switch for position
      {
         case BOTTOM:
            icon_x = MathMax(0, MathMin(hover_mouse_local_x - (icon_size / 2), currentWidth - icon_size)); //--- Set x
            icon_y = currentHeight - icon_size - 2;              //--- Set y
            break;
         case RIGHT:
            icon_y = MathMax(0, MathMin(hover_mouse_local_y - (icon_size / 2), currentHeight - icon_size)); //--- Set y
            icon_x = currentWidth - icon_size - 2;               //--- Set x
            break;
         case BOTTOM_RIGHT:
            icon_x = currentWidth - icon_size - 10;              //--- Set x
            icon_y = currentHeight - icon_size;                  //--- Set y
            break;
         default: break;
      }
      
      canvasGraph.TextOut(icon_x, icon_y, icon_symbol, argb_icon, TA_LEFT | TA_TOP); //--- Draw icon
      canvasGraph.FontAngleSet(0);                               //--- Reset angle
   }
   
   canvasGraph.Update();                                         //--- Update canvas
}

Здесь мы реализуем функцию "UpdateGraphOnCanvas" для отображения графика цен на объекте "canvasGraph", отображающего последние закрытия баров в виде линейного графика с заполненными областями, метками и опциональными индикаторами изменения размера. Начнём с очистки холста методом Erase, установленным на значение «ноль». Если "UseBackground" имеет значение true и "bg_pixels_graph" соответствует текущим размерам, мы в цикле перебираем высоту и ширину, чтобы задать каждый пиксель из масштабированного массива фона, используя метод PixelSet.  Мы преобразуем цвет рамки из "GetBorderColor" в ARGB с полной непрозрачностью с помощью ColorToARGB и рисуем внешнюю и внутреннюю рамки, используя "Line" для верхнего, правого, нижнего и левого краев, создавая эффект двойной рамки.

Мы объявляем и изменяем размер массива с элементами типа double "closePrices" на "graphBars", копируем цены закрытия с помощью CopyClose для текущего инструмента и периода, начиная с нулевого бара, выводим сообщение об ошибке и завершаем работу, если действие не завершено. Аналогично, мы извлекаем times в массив datetime "timeArr" с помощью CopyTime, обрабатывая сбой. Мы находим минимальную и максимальную цены, инициализируясь до первого закрытия и повторяя цикл для обновления, вычисляем диапазон, по умолчанию устанавливая значение _Point при нулевом значении, чтобы избежать проблем с разделением.

Устанавливаем поля, такие как "graphLeft" равные двум, "graphRight" равные ширине минус три, вычисляем эффективную ширину и высоту, а нижнюю границу по оси Y — двум плюс высота. Изменяем размер целочисленных массивов "x_pos" и "y_pos" на "graphBars", затем в цикле нормализуем позиции: x как left плюс нормализованная доля округляемой ширины, y как два плюс масштабированное значение (max минус price) в округляемом диапазоне, используя обратный порядок цен, чтобы последние значения были слева. Устанавливаем цвет линии на синий, преобразуем в ARGB и проходим циклом по сегментам, чтобы нарисовать сглаженные линии с помощью LineAA, меняя местами координаты по оси X для ориентации справа налево.

Для заполнения мы вычисляем минимальное и максимальное перевернутое значение по оси x, проходим циклом по столбцам от минимального до максимального перевернутого значения (справа налево), вычисляем логическое значение x, находим содержащий его сегмент, проверяя позиции, и пропускаем, если такового нет. Мы вычисляем коэффициент интерполяции t, интерполируем y и округляем до верхнего значения y. Затем выполняем цикл от верхнего значения y к нижнему значению y, вычисляем коэффициент затухания снизу, альфа равен 255, умноженному на коэффициент затухания и на "FogOpacity", преобразуем заливку в ARGB с помощью этого альфа-канала. Если "BlendFog" равен true, получаем текущий пиксель с помощью PixelGet, выполняем смешивание с помощью "BlendPixels" и устанавливаем значение; в противном случае устанавливаем значение напрямую с помощью "PixelSet". Для смешивания пикселей мы используем пользовательскую вспомогательную функцию, фрагмент кода которой приведен ниже.

//+------------------------------------------------------------------+
//| Alpha blending function for two ARGB colors                      |
//+------------------------------------------------------------------+
uint BlendPixels(uint bg, uint fg)
{
   uchar bgA = (uchar)((bg >> 24) & 0xFF);                       //--- Get bg alpha
   uchar bgR = (uchar)((bg >> 16) & 0xFF);                       //--- Get bg red
   uchar bgG = (uchar)((bg >> 8) & 0xFF);                        //--- Get bg green
   uchar bgB = (uchar)(bg & 0xFF);                               //--- Get bg blue
   
   uchar fgA = (uchar)((fg >> 24) & 0xFF);                       //--- Get fg alpha
   uchar fgR = (uchar)((fg >> 16) & 0xFF);                       //--- Get fg red
   uchar fgG = (uchar)((fg >> 8) & 0xFF);                        //--- Get fg green
   uchar fgB = (uchar)(fg & 0xFF);                               //--- Get fg blue
   
   if (fgA == 0) return bg;                                      //--- Return bg if transparent
   if (fgA == 255) return fg;                                    //--- Return fg if opaque
   
   double alphaFg = fgA / 255.0;                                 //--- Compute fg alpha
   double alphaBg = 1.0 - alphaFg;                               //--- Compute bg alpha
   
   uchar outR = (uchar)(fgR * alphaFg + bgR * alphaBg);          //--- Blend red
   uchar outG = (uchar)(fgG * alphaFg + bgG * alphaBg);          //--- Blend green
   uchar outB = (uchar)(fgB * alphaFg + bgB * alphaBg);          //--- Blend blue
   uchar outA = (uchar)(fgA + bgA * alphaBg);                    //--- Blend alpha
   
   return (((uint)outA) << 24) | (((uint)outR) << 16) | (((uint)outG) << 8) | ((uint)outB); //--- Return blended
}

Для этой функции мы используем тот же подход, что и для функции ARGB, используя побитовые операции. Для ясности мы добавили комментарии. Продолжая, мы устанавливаем шрифт Arial размером двенадцать, преобразуем цвет текста в соответствии с темой оформления (черный в темный, серый в светлый) в ARGB и рисуем заголовок по центру 'Price Graph' с символом вверху. Мы форматируем самое новое и самое старое значение times с помощью TimeToString с использованием даты и минут, отрисовываем с выравниванием по левому краю в левом нижнем углу, а выравнивание по правому краю в правом нижнем углу.

Если "resize_hovered" или "resizing" имеет значение true, мы переходим в активный режим из "resize_mode" или "hover_mode" и досрочно завершаем работу, если его нет. Мы устанавливаем шрифт значка на Wingdings 3 размером двадцать пять, определяем код и угол на основе режима (внизу/справа как '2'/'1' при нулевом значении, угол '2' - при 450) и преобразуем символ с помощью функции CharToString. Что касается значков, можно выбрать те, которые лучше всего соответствуют вашему стилю. Мы выбрали эти, поскольку в MQL5 еще нет встроенных изменений курсора, поэтому нам пришлось проявить творческий подход. Вот визуализация символов шрифта, которые можно использовать.

SYMBOL FONTS

Далее получаем цвет значка из функции "GetIconColor" с флагом изменения размера, преобразуем его в ARGB, устанавливаем шрифт и угол с помощью FontSet и FontAngleSet, вычисляем положение на основе режима, используя "MathMax"/"MathMin" для ограничения и локальных координат при наведении курсора или фиксированных смещений, рисуем с выравниванием по левому верхнему краю TextOut и сбрасываем угол на ноль. Наконец, для показа графика обновляем холст с помощью Update. При вызове функции в обработчике события инициализации, получаем следующий результат.

GRAPH CANVAS

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

//+------------------------------------------------------------------+
//| Linear interpolation between two colors                          |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   uchar r1 = (uchar)((start >> 16) & 0xFF);                     //--- Get start red
   uchar g1 = (uchar)((start >> 8) & 0xFF);                      //--- Get start green
   uchar b1 = (uchar)(start & 0xFF);                             //--- Get start blue
   
   uchar r2 = (uchar)((end >> 16) & 0xFF);                       //--- Get end red
   uchar g2 = (uchar)((end >> 8) & 0xFF);                        //--- Get end green
   uchar b2 = (uchar)(end & 0xFF);                               //--- Get end blue
   
   uchar r = (uchar)(r1 + factor * (r2 - r1));                   //--- Interpolate red
   uchar g = (uchar)(g1 + factor * (g2 - g1));                   //--- Interpolate green
   uchar b = (uchar)(b1 + factor * (b2 - b1));                   //--- Interpolate blue
   
   return (r << 16) | (g << 8) | b;                              //--- Return color
}

//+------------------------------------------------------------------+
//| Darken a color by a factor (0.0 to 1.0)                         |
//+------------------------------------------------------------------+
color DarkenColor(color colorValue, double factor)
{
   int blue = int((colorValue & 0xFF) * factor);                 //--- Darken blue
   int green = int(((colorValue >> 8) & 0xFF) * factor);         //--- Darken green
   int red = int(((colorValue >> 16) & 0xFF) * factor);          //--- Darken red
   return (color)(blue | (green << 8) | (red << 16));            //--- Return darkened
}

Во-первых, мы реализуем функцию "InterpolateColor" для линейного смешивания двух цветов на основе коэффициента от нуля до единицы, возвращая интерполированный цвет для таких эффектов, как градиенты. Она принимает начальные и конечные цветовые параметры, а также двойной коэффициент. Мы извлекаем красный, зеленый и синий компоненты из начала, используя сдвиги битов вправо на шестнадцать/восемь/ ноль и маскируя их с помощью 0xFF, приведенного к uchar, аналогично для конца, точно так же, как мы делали с другими функциями, основанными на цвете. Мы интерполируем каждый канал как uchar начального значения плюс коэффициент, умноженный на разницу, затем объединяем с красным каналом, сдвинутым влево на шестнадцать, зеленым — на восемь, а синим — на ноль, используя битовые сдвиги и операцию ИЛИ (OR).

Далее создаём функцию "DarkenColor", которая уменьшает яркость цвета на коэффициент от нуля до единицы, где единица сохраняет яркость неизменной, а меньшие значения затемняют цвет, возвращая скорректированный цвет. Функция принимает значение цвета "colorValue" и двойной коэффициент, затемняет синий цвет как int синего, умноженное на коэффициент из маски 0xFF, зеленый цвет из маски со сдвигом на восемь, красный цвет из маски со сдвигом на шестнадцать, и возвращает результат, полученный в результате преобразования цвета в синий ИЛИ зеленый цвет со сдвигом на восемь ИЛИ красный цвет со сдвигом на шестнадцать. Теперь мы можем использовать эти функции в создании статистической панели.

//+------------------------------------------------------------------+
//| Update the stats on the second Canvas                            |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas()
{
   canvasStats.Erase(0);                                         //--- Clear canvas
   
   int statsWidth = currentWidth / 2;                            //--- Compute width
   if (UseBackground && ArraySize(bg_pixels_stats) == statsWidth * currentHeight) //--- Check background
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            canvasStats.PixelSet(x, y, bg_pixels_stats[y * statsWidth + x]); //--- Set pixel
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode
   {
      for (int y = 0; y < currentHeight; y++)                    //--- Loop rows
      {
         double factor = (double)y / (currentHeight - 1);        //--- Compute factor
         color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color
         uchar alpha = (uchar)(255 * BackgroundOpacity);         //--- Compute alpha
         uint argbFill = ColorToARGB(currentColor, alpha);       //--- Convert fill
         
         for (int x = 0; x < statsWidth; x++)                    //--- Loop columns
         {
            uint currentPixel = canvasStats.PixelGet(x, y);      //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend
            canvasStats.PixelSet(x, y, blendedPixel);            //--- Set blended
         }
      }
   }
   
   if (StatsBackgroundMode != NoColor)                           //--- Check mode for borders
   {
      double reduction = BorderOpacityPercentReduction / 100.0;  //--- Compute reduction
      double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity
      uchar alpha = (uchar)(255 * opacity);                      //--- Set alpha
      double darkenReduction = BorderDarkenPercent / 100.0;      //--- Compute darken
      double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Set factor
      
      for (int y = 0; y < currentHeight; y++)                    //--- Loop vertical
      {
         double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (currentHeight - 1); //--- Get factor
         color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base
         color darkColor = DarkenColor(baseColor, darkenFactor); //--- Darken color
         uint argb = ColorToARGB(darkColor, alpha);              //--- Convert to ARGB
         
         canvasStats.PixelSet(0, y, argb);                       //--- Set left outer
         canvasStats.PixelSet(1, y, argb);                       //--- Set left inner
         
         canvasStats.PixelSet(statsWidth - 1, y, argb);          //--- Set right outer
         canvasStats.PixelSet(statsWidth - 2, y, argb);          //--- Set right inner
      }
      
      double factorTop = 0.0;                                    //--- Set top factor
      color baseTop = GetTopColor();                             //--- Get top base
      color darkTop = DarkenColor(baseTop, darkenFactor);        //--- Darken top
      uint argbTop = ColorToARGB(darkTop, alpha);                //--- Convert top
      for (int x = 0; x < statsWidth; x++)                       //--- Loop top
      {
         canvasStats.PixelSet(x, 0, argbTop);                    //--- Set top outer
         canvasStats.PixelSet(x, 1, argbTop);                    //--- Set top inner
      }
      
      double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor
      color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base
      color darkBot = DarkenColor(baseBot, darkenFactor);        //--- Darken bottom
      uint argbBot = ColorToARGB(darkBot, alpha);                //--- Convert bottom
      for (int x = 0; x < statsWidth; x++)                       //--- Loop bottom
      {
         canvasStats.PixelSet(x, currentHeight - 1, argbBot);    //--- Set bottom outer
         canvasStats.PixelSet(x, currentHeight - 2, argbBot);    //--- Set bottom inner
      }
   }
   else                                                          //--- Handle no color
   {
      uint argbBorder = ColorToARGB(GetBorderColor(), 255);      //--- Convert border
      canvasStats.Line(0, 0, statsWidth - 1, 0, argbBorder);     //--- Draw top outer
      canvasStats.Line(statsWidth - 1, 0, statsWidth - 1, currentHeight - 1, argbBorder); //--- Draw right outer
      canvasStats.Line(statsWidth - 1, currentHeight - 1, 0, currentHeight - 1, argbBorder); //--- Draw bottom outer
      canvasStats.Line(0, currentHeight - 1, 0, 0, argbBorder);  //--- Draw left outer
      canvasStats.Line(1, 1, statsWidth - 2, 1, argbBorder);     //--- Draw top inner
      canvasStats.Line(statsWidth - 2, 1, statsWidth - 2, currentHeight - 2, argbBorder); //--- Draw right inner
      canvasStats.Line(statsWidth - 2, currentHeight - 2, 1, currentHeight - 2, argbBorder); //--- Draw bottom inner
      canvasStats.Line(1, currentHeight - 2, 1, 1, argbBorder);  //--- Draw left inner
   }
   
   color labelColor = GetStatsLabelColor();                      //--- Get label color
   color valueColor = GetStatsValueColor();                      //--- Get value color
   color headerColor = GetStatsHeaderColor();                    //--- Get header color
   
   int yPos = 20;                                                //--- Set initial y
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   uint argbHeader = ColorToARGB(headerColor, 255);              //--- Convert header
   canvasStats.TextOut(statsWidth / 2, yPos, "Account Stats", argbHeader, TA_CENTER); //--- Draw account header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   uint argbLabel = ColorToARGB(labelColor, 255);                //--- Convert label
   uint argbValue = ColorToARGB(valueColor, 255);                //--- Convert value
   
   canvasStats.TextOut(10, yPos, "Name:", argbLabel, TA_LEFT);   //--- Draw name label
   canvasStats.TextOut(statsWidth - 10, yPos, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize);       //--- Set header font
   canvasStats.TextOut(statsWidth / 2, yPos, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw bar header
   yPos += 30;                                                   //--- Increment y
   
   canvasStats.FontSet("Arial Bold", StatsFontSize);             //--- Set font
   
   double barOpen = iOpen(_Symbol, _Period, 0);                  //--- Get open
   double barHigh = iHigh(_Symbol, _Period, 0);                  //--- Get high
   double barLow = iLow(_Symbol, _Period, 0);                    //--- Get low
   double barClose = iClose(_Symbol, _Period, 0);                //--- Get close
   
   canvasStats.TextOut(10, yPos, "Open:", argbLabel, TA_LEFT);   //--- Draw open label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barOpen, _Digits), argbValue, TA_RIGHT); //--- Draw open value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "High:", argbLabel, TA_LEFT);   //--- Draw high label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barHigh, _Digits), argbValue, TA_RIGHT); //--- Draw high value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Low:", argbLabel, TA_LEFT);    //--- Draw low label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barLow, _Digits), argbValue, TA_RIGHT); //--- Draw low value
   yPos += 20;                                                   //--- Increment y
   
   canvasStats.TextOut(10, yPos, "Close:", argbLabel, TA_LEFT);  //--- Draw close label
   canvasStats.TextOut(statsWidth - 10, yPos, DoubleToString(barClose, _Digits), argbValue, TA_RIGHT); //--- Draw close value
   
   canvasStats.Update();                                         //--- Update canvas
}

Здесь мы реализуем функцию "UpdateStatsOnCanvas" для отображения панели статистики в объекте "canvasStats", отображающей сведения о торговом счете и текущем баре с соответствующей темой фона, заливками, рамками и текстом. Выполняем очистку холста методом Erase, установленным на значение «ноль». Если "UseBackground" имеет значение true и "bg_pixels_stats" соответствует размерам (половина ширины графика умножается на высоту), мы перебираем строки и столбцы, чтобы задать каждый пиксель из масштабированного массива фона, используя метод PixelSet

Если "StatsBackgroundMode" не является "NoColor", мы в цикле перебираем высоту, чтобы вычислить коэффициент вертикали, определяем цвет строки как "GetTopColor", если это одиночный режим, или интерполируем между верхом и низом с помощью "InterpolateColor", если градиент, рассчитываем альфа из "BackgroundOpacity", умноженного на 255, преобразуем в ARGB. Для каждого x в строке найдем текущий пиксель с помощью PixelGet, смешаем его с заливкой, используя "BlendPixels", и установим смешанный пиксель.

Для рамок в режимах заливки мы вычисляем уменьшенную непрозрачность как "BorderOpacityPercentReduction", деленное на 100,0, с ограничением значения от 0,0 до 1,0, умноженного на "BackgroundOpacity", альфа-канал как 255, а коэффициент затемнения как 1,0 минус "BorderDarkenPercent", деленное на 100,0, с ограничением значения. Пройдем циклом по оси Y, чтобы получить коэффициент строки (0,0, если строка одиночная, иначе нормализованная), выберем базовый цвет как верхний или интерполированный, затемним с помощью функции "DarkenColor", преобразуем в ARGB с альфа-каналом, установим левые внешние/внутренние и правые внешние/внутренние пиксели. Для верхнего ряда используем коэффициент 0,0, базовый верхний цвет, затемнение, ARGB, цикл x для установки верхнего цвета внешним/внутренним. Для нижней части установим коэффициент 1,0 или 0,0, если используется одиночный цвет, базовый нижний или верхний, затемним, выберем ARGB, установим нижний внешний/внутренний. Если заливка отсутствует, преобразуем рамку из "GetBorderColor" в ARGB при 255, нарисуем внешние и внутренние горизонтальные/вертикальные линии с помощью "Line" для верхнего / правого / нижнего / левого края.

Извлекаем цвета темы оформления для меток, значений и заголовков с помощью методов-получателей. Установим начальное значение y равным двадцати, шрифт установим Arial Bold в "StatsHeaderFontSize", преобразуем заголовок в ARGB, отрисуем статистику счета Account Stats по центру с помощью TextOut, увеличим y на тридцать.

Установим шрифт Arial Bold в "StatsFontSize", преобразуем метку и значение в ARGB. Нарисуем Name, выровненное по левому краю: на x десять, выровненное имя счета по правому краю от AccountInfoString с помощью ACCOUNT_NAME шириной минус десять, увеличим y на двадцать. Аналогично, для Balance: с помощью AccountInfoDouble "ACCOUNT_BALANCE" до двух знаков после запятой, Equity: с помощью ACCOUNT_EQUITY. Увеличим значение y на тридцать, установим шрифт заголовка, нарисуем по центру Current Bar Stats, увеличьте значение y на тридцать. Установим шрифт обратно, выберем значение открытия/максимума/минимума/закрытия текущего бара с помощью iOpen/"iHigh"/"iLow"/iClose для символа/периода/нулевого бара. Нарисуем метку Open: и значение _Digits десятичных чисел по правому краю, увеличим y на двадцать; повторим для High:, Low:, Close:. Наконец, для отображения статистики обновляем холст с помощью Update. После компиляции получаем следующий результат.

STATISTICS PANEL

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

//+------------------------------------------------------------------+
//| Color selection WITHOUT interpolation (hard switch only)         |
//+------------------------------------------------------------------+
color InterpolateColor(color start, color end, double factor)
{
   // Clamp factor just to be safe
   if(factor <= 0.0)
      return start;

   if(factor >= 1.0)
      return end;

   // HARD boundary — no mixing at all
   return (factor < 0.5 ? start : end);
}

При использовании этого подхода получаем следующий результат.

HARD BOUNDARY COLOR MIXING RESULT

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

//+------------------------------------------------------------------+
//| Initialize expert                                                |
//+------------------------------------------------------------------+
int OnInit()

   //--- Existing init logic
   
   DrawHeaderOnCanvas();                                         //--- Draw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);             //--- Enable mouse events
   ChartRedraw();                                                //--- Redraw chart
   return(INIT_SUCCEEDED);                                       //--- Return success
}

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

//+------------------------------------------------------------------+
//| Check if mouse is over header (excluding buttons)                |
//+------------------------------------------------------------------+
bool IsMouseOverHeader(int mouse_x, int mouse_y)
{
   int header_x = currentCanvasX;                                //--- Get header x
   int header_y = currentCanvasY;                                //--- Get header y
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int header_h = header_height;                                 //--- Get height
   
   if (mouse_x < header_x || mouse_x > header_x + header_w || mouse_y < header_y || mouse_y > header_y + header_h) return false; //--- Check outside
   
   int theme_left = header_x + header_w + theme_x_offset - button_size / 2; //--- Compute theme left
   int theme_right = theme_left + button_size;                   //--- Compute theme right
   int theme_top = header_y;                                     //--- Set theme top
   int theme_bottom = theme_top + header_h;                      //--- Compute theme bottom
   if (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom) return false; //--- Check in theme
   
   int min_left = header_x + header_w + minimize_x_offset - button_size / 2; //--- Compute minimize left
   int min_right = min_left + button_size;                       //--- Compute minimize right
   int min_top = header_y;                                       //--- Set minimize top
   int min_bottom = min_top + header_h;                          //--- Compute minimize bottom
   if (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom) return false; //--- Check in minimize
   
   int close_left = header_x + header_w + close_x_offset - button_size / 2; //--- Compute close left
   int close_right = close_left + button_size;                   //--- Compute close right
   int close_top = header_y;                                     //--- Set close top
   int close_bottom = close_top + header_h;                      //--- Compute close bottom
   if (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom) return false; //--- Check in close
   
   return true;                                                  //--- Return in header
}

//+------------------------------------------------------------------+
//| Check if mouse over theme button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverTheme(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int theme_left = currentCanvasX + header_w + theme_x_offset - button_size / 2; //--- Compute left
   int theme_right = theme_left + button_size;                   //--- Compute right
   int theme_top = currentCanvasY;                               //--- Set top
   int theme_bottom = theme_top + header_height;                 //--- Compute bottom
   return (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom); //--- Check in theme
}

//+------------------------------------------------------------------+
//| Check if mouse over minimize button                              |
//+------------------------------------------------------------------+
bool IsMouseOverMinimize(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int min_left = currentCanvasX + header_w + minimize_x_offset - button_size / 2; //--- Compute left
   int min_right = min_left + button_size;                       //--- Compute right
   int min_top = currentCanvasY;                                 //--- Set top
   int min_bottom = min_top + header_height;                     //--- Compute bottom
   return (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom); //--- Check in minimize
}

//+------------------------------------------------------------------+
//| Check if mouse over close button                                 |
//+------------------------------------------------------------------+
bool IsMouseOverClose(int mouse_x, int mouse_y)
{
   int header_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute width
   int close_left = currentCanvasX + header_w + close_x_offset - button_size / 2; //--- Compute left
   int close_right = close_left + button_size;                   //--- Compute right
   int close_top = currentCanvasY;                               //--- Set top
   int close_bottom = close_top + header_height;                 //--- Compute bottom
   return (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom); //--- Check in close
}

//+------------------------------------------------------------------+
//| Check if mouse over resize borders                               |
//+------------------------------------------------------------------+
bool IsMouseOverResize(int mx, int my, ENUM_RESIZE_MODE &rmode)
{
   if (panels_minimized) return false;                           //--- Check if minimized
   int graph_x = currentCanvasX;                                 //--- Get graph x
   int graph_y = currentCanvasY + header_height + gap_y;         //--- Get graph y
   int graph_right = graph_x + currentWidth;                     //--- Compute right
   int graph_bottom = graph_y + currentHeight;                   //--- Compute bottom
   bool over_right = (mx >= graph_right - resize_thickness && mx <= graph_right + resize_thickness) && (my >= graph_y && my <= graph_bottom); //--- Check right
   bool over_bottom = (my >= graph_bottom - resize_thickness && my <= graph_bottom + resize_thickness) && (mx >= graph_x && mx <= graph_right); //--- Check bottom
   if (over_bottom && over_right)                                //--- Check corner
   {
      rmode = BOTTOM_RIGHT;                                      //--- Set bottom-right
      return true;                                               //--- Return true
   }
   else if (over_bottom)                                         //--- Check bottom only
   {
      rmode = BOTTOM;                                            //--- Set bottom
      return true;                                               //--- Return true
   }
   else if (over_right)                                          //--- Check right only
   {
      rmode = RIGHT;                                             //--- Set right
      return true;                                               //--- Return true
   }
   return false;                                                 //--- Return false
}

//+------------------------------------------------------------------+
//| Toggle theme                                                     |
//+------------------------------------------------------------------+
void ToggleTheme()
{
   is_dark_theme = !is_dark_theme;                               //--- Switch theme
   Print("Switched to ", (is_dark_theme ? "Dark" : "Light"), " theme"); //--- Print switch
   DrawHeaderOnCanvas();                                         //--- Redraw header
   UpdateGraphOnCanvas();                                        //--- Update graph
   if (EnableStatsPanel) UpdateStatsOnCanvas();                  //--- Update stats
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Toggle minimize state                                            |
//+------------------------------------------------------------------+
void ToggleMinimize()
{
   panels_minimized = !panels_minimized;                         //--- Toggle minimized
   if (panels_minimized)                                         //--- Handle minimize
   {
      canvasGraph.Destroy();                                     //--- Destroy graph
      graphCreated = false;                                      //--- Reset graph flag
      if (EnableStatsPanel)                                      //--- Check stats
      {
         canvasStats.Destroy();                                  //--- Destroy stats
         statsCreated = false;                                   //--- Reset stats flag
      }
   }
   else                                                          //--- Handle maximize
   {
      if (!canvasGraph.CreateBitmapLabel(0, 0, canvasGraphName, currentCanvasX, currentCanvasY + header_height + gap_y, currentWidth, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate graph
      {
         Print("Failed to recreate Graph Canvas");               //--- Print error
      }
      graphCreated = true;                                       //--- Set graph flag
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel)                                      //--- Check stats
      {
         int statsX = currentCanvasX + currentWidth + PanelGap;  //--- Compute stats X
         if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) //--- Recreate stats
         {
            Print("Failed to recreate Stats Canvas");            //--- Print error
         }
         statsCreated = true;                                    //--- Set stats flag
         UpdateStatsOnCanvas();                                  //--- Update stats
      }
   }
   
   int new_header_width = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute new width
   canvasHeader.Resize(new_header_width, header_height);         //--- Resize header
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, new_header_width); //--- Update header width
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
   DrawHeaderOnCanvas();                                         //--- Redraw header
   canvasHeader.Update();                                        //--- Update header
   ChartRedraw();                                                //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Close the dashboard                                              |
//+------------------------------------------------------------------+
void CloseDashboard()
{
   canvasHeader.Destroy();                                       //--- Destroy header
   canvasGraph.Destroy();                                        //--- Destroy graph
   if (EnableStatsPanel) canvasStats.Destroy();                  //--- Destroy stats
   ChartRedraw();                                                //--- Redraw chart
}

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

Мы также добавляем функции переключения темы и сворачивания панели. Для этого меняем соответствующие флаги, перерисовываем холсты и обновляем график. А также выполняем эти действия для сворачивания путем переключения состояния, уничтожая/создавая заново холсты графиков и статистики по мере необходимости, изменяя размер заголовка, перерисовывая его и обновляя. Наконец, мы определяем функцию закрытия, которая уничтожает все холсты и перерисовывает график. Мы определяем функцию "IsMouseOverHeader", чтобы проверить, находится ли мышь над областью заголовка без перекрывающихся кнопок, возвращая логическое значение. Она получает позицию заголовка из "currentCanvasX" и "currentCanvasY", вычисляет ширину, включая опциональную статистику и разрыв, если панель не свернута, высоту из "header_height" и возвращает значение false, если выходит за границы.

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

//+------------------------------------------------------------------+
//| Handle chart event                                               |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
   if (id == CHARTEVENT_CHART_CHANGE)                            //--- Check change event
   {
      DrawHeaderOnCanvas();                                      //--- Redraw header
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
   }
   else if (id == CHARTEVENT_MOUSE_MOVE)                         //--- Handle mouse move
   {
      int mouse_x = (int)lparam;                                 //--- Get mouse x
      int mouse_y = (int)dparam;                                 //--- Get mouse y
      int mouse_state = (int)sparam;                             //--- Get mouse state
      
      bool prev_header_hovered = header_hovered;                 //--- Store previous header
      bool prev_min_hovered = minimize_hovered;                  //--- Store previous minimize
      bool prev_close_hovered = close_hovered;                   //--- Store previous close
      bool prev_theme_hovered = theme_hovered;                   //--- Store previous theme
      bool prev_resize_hovered = resize_hovered;                 //--- Store previous resize
      
      header_hovered = IsMouseOverHeader(mouse_x, mouse_y);      //--- Check header hover
      theme_hovered = IsMouseOverTheme(mouse_x, mouse_y);        //--- Check theme hover
      minimize_hovered = IsMouseOverMinimize(mouse_x, mouse_y); //--- Check minimize hover
      close_hovered = IsMouseOverClose(mouse_x, mouse_y);        //--- Check close hover
      resize_hovered = IsMouseOverResize(mouse_x, mouse_y, hover_mode); //--- Check resize hover
      
      if (resize_hovered || resizing)                            //--- Check resize state
      {
         hover_mouse_local_x = mouse_x - currentCanvasX;         //--- Set local x
         hover_mouse_local_y = mouse_y - (currentCanvasY + header_height + gap_y); //--- Set local y
      }
      
      bool hover_changed = (prev_header_hovered != header_hovered || prev_min_hovered != minimize_hovered || 
                            prev_close_hovered != close_hovered || prev_theme_hovered != theme_hovered ||
                            prev_resize_hovered != resize_hovered); //--- Check change
      
      if (hover_changed)                                         //--- If changed
      {
         DrawHeaderOnCanvas();                                   //--- Redraw header
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      else if ((resize_hovered || resizing) && (mouse_x != last_mouse_x || mouse_y != last_mouse_y)) //--- Check position change
      {
         UpdateGraphOnCanvas();                                  //--- Update graph
         ChartRedraw();                                          //--- Redraw chart
      }
      
      string header_tooltip = "";                                //--- Initialize tooltip
      if (theme_hovered) header_tooltip = "Toggle Theme (Dark/Light)"; //--- Set theme tooltip
      else if (minimize_hovered) header_tooltip = panels_minimized ? "Maximize Panels" : "Minimize Panels"; //--- Set minimize tooltip
      else if (close_hovered) header_tooltip = "Close Dashboard"; //--- Set close tooltip
      ObjectSetString(0, canvasHeaderName, OBJPROP_TOOLTIP, header_tooltip); //--- Set header tooltip
      
      string resize_tooltip = "";                                //--- Initialize resize tooltip
      if (resize_hovered || resizing)                            //--- Check resize
      {
         ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get mode
         switch (active_mode)                                    //--- Switch mode
         {
            case BOTTOM: resize_tooltip = "Resize Bottom"; break; //--- Set bottom
            case RIGHT: resize_tooltip = "Resize Right"; break;  //--- Set right
            case BOTTOM_RIGHT: resize_tooltip = "Resize Bottom-Right"; break; //--- Set corner
            default: break;
         }
      }
      ObjectSetString(0, canvasGraphName, OBJPROP_TOOLTIP, resize_tooltip); //--- Set graph tooltip
      
      if (mouse_state == 1 && prev_mouse_state == 0)             //--- Check mouse down
      {
         if (header_hovered)                                     //--- Check header
         {
            panel_dragging = true;                               //--- Start drag
            panel_drag_x = mouse_x;                              //--- Set drag x
            panel_drag_y = mouse_y;                              //--- Set drag y
            panel_start_x = currentCanvasX;                      //--- Set start x
            panel_start_y = currentCanvasY;                      //--- Set start y
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);       //--- Disable scroll
            DrawHeaderOnCanvas();                                //--- Show drag color
            ChartRedraw();                                       //--- Redraw chart
         }
         else if (theme_hovered)                                 //--- Check theme
         {
            ToggleTheme();                                       //--- Toggle theme
         }
         else if (minimize_hovered)                              //--- Check minimize
         {
            ToggleMinimize();                                    //--- Toggle minimize
         }
         else if (close_hovered)                                 //--- Check close
         {
            CloseDashboard();                                    //--- Close dashboard
         }
         else                                                    //--- Handle resize
         {
            ENUM_RESIZE_MODE temp_mode = NONE;                   //--- Initialize temp
            if (!panel_dragging && !resizing && IsMouseOverResize(mouse_x, mouse_y, temp_mode)) //--- Check resize
            {
               resizing = true;                                  //--- Start resizing
               resize_mode = temp_mode;                          //--- Set mode
               resize_start_x = mouse_x;                         //--- Set start x
               resize_start_y = mouse_y;                         //--- Set start y
               start_width = currentWidth;                       //--- Set start width
               start_height = currentHeight;                     //--- Set start height
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);    //--- Disable scroll
               UpdateGraphOnCanvas();                            //--- Show icon
               ChartRedraw();                                    //--- Redraw chart
            }
         }
      }
      else if (panel_dragging && mouse_state == 1)               //--- Handle dragging
      {
         int dx = mouse_x - panel_drag_x;                        //--- Compute dx
         int dy = mouse_y - panel_drag_y;                        //--- Compute dy
         int new_x = panel_start_x + dx;                         //--- Compute new x
         int new_y = panel_start_y + dy;                         //--- Compute new y
         
         int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
         int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
         int full_w = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute full width
         int full_h = header_height + gap_y + (panels_minimized ? 0 : currentHeight); //--- Compute full height
         new_x = MathMax(0, MathMin(chart_w - full_w, new_x));   //--- Clamp x
         new_y = MathMax(0, MathMin(chart_h - full_h, new_y));   //--- Clamp y
         
         currentCanvasX = new_x;                                 //--- Update x
         currentCanvasY = new_y;                                 //--- Update y
         
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, new_x); //--- Update header x
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, new_y); //--- Update header y
         if (!panels_minimized)                                  //--- Check if shown
         {
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XDISTANCE, new_x); //--- Update graph x
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update graph y
            if (EnableStatsPanel)                                //--- Check stats
            {
               int statsX = new_x + currentWidth + PanelGap;     //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, statsX); //--- Update stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Update stats y
            }
         }
         ChartRedraw();                                          //--- Redraw chart
      }
      else if (resizing && mouse_state == 1)                     //--- Handle resizing
      {
         int dx = mouse_x - resize_start_x;                      //--- Compute dx
         int dy = mouse_y - resize_start_y;                      //--- Compute dy
         int new_width = currentWidth;                           //--- Initialize new width
         int new_height = currentHeight;                         //--- Initialize new height
         if (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT) //--- Check right
         {
            new_width = MathMax(min_width, start_width + dx);    //--- Update width
         }
         if (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT) //--- Check bottom
         {
            new_height = MathMax(min_height, start_height + dy); //--- Update height
         }
         
         if (new_width != currentWidth || new_height != currentHeight) //--- Check change
         {
            int chart_w = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); //--- Get chart width
            int chart_h = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
            int avail_w = chart_w - currentCanvasX;              //--- Compute available width
            int avail_h = chart_h - (currentCanvasY + header_height + gap_y); //--- Compute available height
            new_height = MathMin(new_height, avail_h);           //--- Clamp height
            if (EnableStatsPanel)                                //--- Check stats
            {
               double max_w_d = (avail_w - PanelGap) / 1.5;      //--- Compute max width
               int max_w = (int)MathFloor(max_w_d);              //--- Floor max
               new_width = MathMin(new_width, max_w);            //--- Clamp width
            }
            else                                                 //--- No stats
            {
               new_width = MathMin(new_width, avail_w);          //--- Clamp width
            }
            
            currentWidth = new_width;                            //--- Update width
            currentHeight = new_height;                          //--- Update height
            
            if (UseBackground && ArraySize(original_bg_pixels) > 0) //--- Check background
            {
               ArrayCopy(bg_pixels_graph, original_bg_pixels);   //--- Copy graph
               ScaleImage(bg_pixels_graph, (int)orig_w, (int)orig_h, currentWidth, currentHeight); //--- Scale graph
               if (EnableStatsPanel)                             //--- Check stats
               {
                  ArrayCopy(bg_pixels_stats, original_bg_pixels); //--- Copy stats
                  ScaleImage(bg_pixels_stats, (int)orig_w, (int)orig_h, currentWidth / 2, currentHeight); //--- Scale stats
               }
            }
            
            canvasGraph.Resize(currentWidth, currentHeight);     //--- Resize graph
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XSIZE, currentWidth); //--- Update graph width
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YSIZE, currentHeight); //--- Update graph height
            
            if (EnableStatsPanel)                                //--- Check stats
            {
               int stats_width = currentWidth / 2;               //--- Compute stats width
               canvasStats.Resize(stats_width, currentHeight);   //--- Resize stats
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XSIZE, stats_width); //--- Update stats width
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YSIZE, currentHeight); //--- Update stats height
               int stats_x = currentCanvasX + currentWidth + PanelGap; //--- Compute stats x
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, stats_x); //--- Update stats x
            }
            
            canvasHeader.Resize(currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0), header_height); //--- Resize header
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0)); //--- Update header width
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height); //--- Update header height
            
            DrawHeaderOnCanvas();                                //--- Redraw header
            UpdateGraphOnCanvas();                               //--- Update graph
            if (EnableStatsPanel) UpdateStatsOnCanvas();         //--- Update stats
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      else if (mouse_state == 0 && prev_mouse_state == 1)        //--- Check mouse up
      {
         if (panel_dragging)                                     //--- Check dragging
         {
            panel_dragging = false;                              //--- Stop drag
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            DrawHeaderOnCanvas();                                //--- Reset color
            ChartRedraw();                                       //--- Redraw chart
         }
         if (resizing)                                           //--- Check resizing
         {
            resizing = false;                                    //--- Stop resize
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true);        //--- Enable scroll
            UpdateGraphOnCanvas();                               //--- Remove icon
            ChartRedraw();                                       //--- Redraw chart
         }
      }
      
      last_mouse_x = mouse_x;                                    //--- Update last x
      last_mouse_y = mouse_y;                                    //--- Update last y
      prev_mouse_state = mouse_state;                            //--- Update state
   }
}

В обработчике OnChartEvent, если id равен CHARTEVENT_CHART_CHANGE, вызываем метод "DrawHeaderOnCanvas" для перерисовки заголовка, "UpdateGraphOnCanvas" для графика, "UpdateStatsOnCanvas", если "EnableStatsPanel" имеет значение true, и ChartRedraw для обновления отображения. Для CHARTEVENT_MOUSE_MOVE мы преобразуем lparam в значение x мыши, dparam в значение y, а sparam в значение state в виде целых чисел. Мы сохраняем предыдущие состояния наведения курсора в локальных переменных, затем обновляем "header_hovered" с помощью "IsMouseOverHeader", "theme_hovered" с помощью "IsMouseOverTheme", "minimize_hovered" с помощью "IsMouseOverMinimize", "close_hovered" с помощью "IsMouseOverClose" и "resize_hovered" с помощью "IsMouseOverResize", передавая ссылку на переменную "hover_mode". Если выполняется наведение на область изменения размера или значение "resizing" равно true, установим "hover_mouse_local_x" и "hover_mouse_local_y" относительно положения на графике. Проверяем, изменилось ли какое-либо состояние наведения курсора, сравнивая предыдущее с текущим, и если да, то перерисовываем заголовок и график, а затем перерисовываем заново. В противном случае, если состояние изменения размера и положение мыши отличаются от "last_mouse_x"/"last_mouse_y", обновим график и перерисуем заново.

Инициализируем строку всплывающей подсказки заголовка, устанавливаем значение для переключения темы Toggle Theme (темная/светлая), если "theme_hovered", разворачиваем/сворачиваем сообщение в зависимости от значения "panels_minimized", если "minimize_hovered", закрываем панель с помощью Close Dashboard, если "close_hovered", и применяем к "canvasHeaderName" с помощью ObjectSetString используя OBJPROP_TOOLTIP. Аналогично, для всплывающей подсказки по изменению размера определим активный режим из "resize_mode" или "hover_mode", установим строку, основанную на bottom/right/bottom-right, и применим к "canvasGraphName".

Если состояние мыши равно единице, а "prev_mouse_state" равно нулю при нажатии вниз: если "header_hovered", установим "panel_dragging" в значение true, сохраним координаты перетаскивания/начала, отключим прокрутку графика мышью с помощью ChartSetInteger" CHART_MOUSE_SCROLL" в значение false, перерисуем заголовок, перерисуем график; в противном случае, если "theme_hovered", вызываем "ToggleTheme"; если "minimize_hovered", вызываем "ToggleMinimize"; если "close_hovered", вызываем "CloseDashboard"; в противном случае, если перетаскивание/изменение размера не выполняется и "IsMouseOverResize" в значении true, переходим в режим temp, установим "resizing" в значение true, "resize_mode" в режим temp, сохраним координаты начала/размеры, отключим прокрутку, обновим график, перерисуем.

Если параметр "panel_dragging" имеет значение true и значение one для удержания, вычислим дельты, новые координаты x/y от начала плюс дельты, получим ширину/высоту графика с помощью ChartGetInteger  "CHART_WIDTH_IN_PIXELS"/CHART_HEIGHT_IN_PIXELS, полную ширину/высоту панели, включая опциональные статистические данные/пробелы/заголовок, если панель не свернута, ограничим новые координаты x/y нулем до величины графика минус полные размеры с помощью "MathMax"/"MathMin", обновим "currentCanvasX"/"currentCanvasY", установим расстояния объекта по осям x/y для заголовка, и если панель не свернута для графика и опциональных статистических данных (вычисление статистики x), то перерисуем.

Если "resizing" равно true и указано значение один для состояния, вычислим дельты, инициализируем новую ширину/высоту текущими значениями, добавим dx, если режим "справа" или "угла" ограничен значением "min_width", а dy для нижнего или углового положения — значением "min_height". Если переменные изменены, получим размеры графика, доступную ширину/высоту из текущей позиции, ограничим новое значение высоты доступной высотой, для ширины, если статистика ограничивает ее до минимального значения (доступная ширина минус зазор)/1,5, в противном случае — до доступной высоты, обновим текущие значения. Если используется фон, скопируем исходные пиксели в graph/stats, масштабируем с помощью функции "ScaleImage" до новых размеров (stats — половина ширины). Изменим размер элемента "canvasGraph" с помощью функции Resize и установим значения "OBJPROP_XSIZE"/"YSIZE", аналогично для статистики, если включена, обновив положение по оси X и заголовок до полной ширины. Перерисуем заголовок/график/статистика, если эта функция включена, перерисуем график.

Если состояние равно нулю, а "prev_mouse_state" равно единице (вверх): если "panel_dragging", установим значение false, включим прокрутку, установим значение true, перерисуем заголовок, перерисуем график; если "resizing", установим значение false, включим прокрутку, обновим график, перерисуем график. Обновляем "last_mouse_x"/"last_mouse_y" до текущих значений, "prev_mouse_state" до значения состояния. Нам также необходимо обновлять панель по каждому тику для отражения новых цен.

//+------------------------------------------------------------------+
//| Handle tick event                                                |
//+------------------------------------------------------------------+
void OnTick()
{
   static datetime lastBarTime = 0;                              //--- Initialize last time
   datetime currentBarTime = iTime(_Symbol, _Period, 0);         //--- Get current time
   if (currentBarTime > lastBarTime)                             //--- Check new bar
   {
      UpdateGraphOnCanvas();                                     //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas();               //--- Update stats
      ChartRedraw();                                             //--- Redraw chart
      lastBarTime = currentBarTime;                              //--- Update last time
   }
}

В обработчике OnTick используем статическую переменную типа datetime "lastBarTime", инициализированную нулем, для отслеживания времени открытия предыдущего бара, а также получаем время текущего бара с помощью iTime для инструмента, периода и нулевого бара. Если текущее время больше, чем "lastBarTime", что указывает на новый бар, мы вызываем "UpdateGraphOnCanvas" для обновления графика цены, "UpdateStatsOnCanvas", если "EnableStatsPanel" имеет значение true для статистики, перерисовываем график и обновляем "lastBarTime" до текущего значения. Наконец, во избежание загромождения необходимо удалять отрисованные объекты, когда они не нужны.

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   canvasHeader.Destroy();                                       //--- Destroy header
   if (graphCreated) canvasGraph.Destroy();                      //--- Destroy graph if created
   if (statsCreated) canvasStats.Destroy();                      //--- Destroy stats if created
   ChartRedraw();                                                //--- Redraw chart
}

В обработчике OnDeinit уничтожаем холст заголовка с помощью "canvasHeader.Destroy", затем условно уничтожаем холст графика, если "graphCreated" имеет значение true, с помощью "canvasGraph.Destroy", и холст статистики, если "statsCreated" имеет значение true, с помощью "canvasStats.Destroy". Наконец, перерисовываем график, чтобы убедиться, что все остатки удалены с дисплея. После компиляции получаем следующий результат.

CANVAS DASHBOARD TEST

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


Тестирование на истории

Мы провели тестирование, а ниже представлена скомпилированная визуализация в едином формате растрового изображения Graphics Interchange Format (GIF).

CANVAS DASHBOARD BACKTEST


Заключение

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

Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21038

Прикрепленные файлы |
Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами
В этой статье мы разработаем систему анализа пробоев на Лондонской сессии, которая будет определять пробои диапазона перед открытием сессии и выставлять отложенные ордера с настройкой типа сделок и параметров риска. Мы реализуем в системе трейлинг-стоп, соотношение риска и прибыли, контроль максимальной просадки, а также панель управления для мониторинга в режиме реального времени.
Нейросети в трейдинге: Адаптивное масштабирование представлений (Окончание) Нейросети в трейдинге: Адаптивное масштабирование представлений (Окончание)
В статье представлена интеграция ранее реализованных компонентов фреймворка ADS в прикладную торговую модель и их проверка на исторических данных. Показано, как построение объекта верхнего уровня позволяет встроить сложную архитектуру в существующие решения, сохранив управляемость и прозрачность модели. Проведенное тестирование раскрывает как потенциал подхода в генерации прибыли, так и его ограничения, формируя основу для дальнейшей оптимизации риск-менеджмента и повышения устойчивости системы.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python
N-BEATS — это революционная модель глубокого обучения, разработанная для прогнозирования временных рядов. Она была выпущена в попытке превзойти возможности классических моделей прогнозирования временных рядов, таких как ARIMA, PROPHET, VAR и др. Познакомимся с данной моделью и посмотрим на возможности ее применения для прогнозирования фондового рынка.