Торговые инструменты на MQL5 (Часть 13): Создание ценовой панели на базе Canvas с панелями графика и статистики
Введение
В своей предыдущей статье (Часть 12) мы расширили панель корреляционной матрицы в MetaQuotes Language 5 (MQL5) за счет интерактивных функций для повышения удобства использования. В части 13 мы представляем ценовую панель на основе холста с панелями графиков и статистики, предназначенную для предоставления трейдерам четкой и действенной информации о движении цен и показателях счета. Используя класс CCanvas, эта панель предлагает панели с возможностью перетаскивания и изменения размера, отображает недавнюю динамику цен и жизненно важную статистику, включая баланс, эквити и значения открытия/максимума/минимума/закрытия текущего бара. Она еще больше обогащает наши торговые решения за счет использования настраиваемых фонов, переключения тем оформления и обновлений в режиме реального времени. Эти инструменты позволяют более эффективно отслеживать изменения на рынке и реагировать на них. В статье рассмотрим следующие темы:
- Изучение структуры ценовой панели на основе холста
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет функциональная панель на основе холста в MQL5 для мониторинга цен и показателей, готовая к настройке. Перейдём к реализации!
Изучение структуры ценовой панели на основе холста
В структуре ценовой панели на основе холста используется класс CCanvas в MQL5 для создания пользовательских графических панелей в целях отображения ценовых данных и показателей счета в режиме реального времени, предлагая компактную интерактивную альтернативу стандартным графическим индикаторам для пользователей, которым требуется быстрый визуальный обзор без нагромождений на главном графике. Панель состоит из двух частей: главной графической панели, отображающей последние закрытия баров в виде линии с заполненными областями и эффектами тумана для придания глубины, а также дополнительной панели статистики, отображающей данные счета, такие как баланс / эквити и текущее значение OHLC бара, которые поддерживаются фоновыми изображениями с наложением непрозрачности, градиентной или сплошной заливкой и двойными рамками для обеспечения эстетики.
Улучшения включают в себя перетаскивание с помощью мыши для изменения положения, изменение размера с помощью наведения курсора мыши на границы и захватов значков для обратной связи, переключение между сворачиванием и разворачиванием панелей, переключение тем между темными и светлыми режимами для настройки цветов, а также обновление в режиме реального времени на новых барах для отображения последних цен и статистики.
Эти возможности используют обработку событий для взаимодействия с мышью, бикубическое масштабирование для плавного изменения размера изображения, альфа-смешивание для оверлеев, таких как туман, и управление цветами ARGB для прозрачности, обеспечивая реагирование и настраиваемость панели без использования встроенных объектов MQL5, что было нашей главной целью, поскольку мы уже пару раз использовали встроенные объекты. В этот раз мы меняем подход и вместо этого полностью исследуем признаки холста.
Наш план состоит в том, чтобы включить библиотеку canvas, определить входные параметры для позиций / размеров / цветов / непрозрачности / режимов, загрузить и масштабировать ресурс фонового изображения, создать отдельные холсты для заголовка / графика / статистики с проверками создания, реализовать функции рисования для заголовков с помощью значков / всплывающих подсказок / рамок, графиков с отображением цен /наполнением / временными метками / изменением размера значков и статистику с соответствующим теме оформления текстом / градиентами / затемненными рамками, добавлять вспомогательные функции для интерполяции цветов / затемнения / смешивания / выделения ARGB, а также обрабатывать события графика для событий наведения курсора мыши / перетаскивания / изменения размера / переключения с фиксацией размеров / минимальными размерами, с обновлением по тикам для получения новых данных. Вкратце, вот наглядное представление наших целей.

Реализация средствами 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" для жестко заданного фонового изображения. Изображение, которое вы прикрепляете, должно быть только в виде растрового файла. Это можно легко это сделать, и наш файл изображения теперь выглядит следующим образом. Для простоты мы импортировали его туда, где находится файл программы. Посмотрим на то, что у нас получилось.

После подготовки файла изображения мы продолжаем с глобальными переменными: массивы "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.
Важно понимать, что вам не обязательно использовать бикубический подход. Вы можете использовать линейный или билинейный подход, но это даст более неровное изображение, чем то, которое нам на самом деле нужно. Таким образом, мы используем наилучший подход для сглаживания пикселизации изображения. На самом деле, ниже вы можете ознакомиться с различными подходами, которые у вас могут быть.

Мы видим, что метод бикубической интерполяции обеспечивает наиболее плавный просмотр изображения по сравнению с другими подходами. Далее с помощью функций изменим размер нашего исходного изображения таким образом, чтобы оно соответствовало динамически. Наш графический файл значительно больше, чем область холста, на которой мы хотим его отрисовать, поэтому нам нужно изменить его размер. Реализуем это в обработчике события инициализации.
//+------------------------------------------------------------------+ //| 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. Это лишь создаёт области отрисовки, но сами элементы ещё не нарисованы. Чтобы продемонстрировать это, вот образец того, что мы можем увидеть во всплывающих подсказках при компиляции.

Мы видим, что теперь нарисованы области холста. Теперь нужно создать сам чертеж для визуализации объектов холст. Мы начнем с заголовка, но поскольку нам нужно учитывать тему оформления, сначала определим некоторые вспомогательные функции темы, которые позволят нам создать рендеринг темной или светлой темы, где это применимо.
//+------------------------------------------------------------------+ //| 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". При вызове функции во время инициализации получаем следующий результат.

На изображении видно, что заголовок холста отмечен корректно. Далее надо нарисовать на холсте графика область, где предполагается провести анализ цен и нарисовать их линейный график. Таким образом, по сути, мы рисуем наш собственный простой график цен. Однако обратите внимание, что это всего лишь наш подход; он является самым простым, на наш взгляд, для данной демонстрации. Вы можете использовать собственные сложные расчеты по своему усмотрению. Вот подход, которую мы используем для выполнения этой задачи.
//+------------------------------------------------------------------+ //| 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 еще нет встроенных изменений курсора, поэтому нам пришлось проявить творческий подход. Вот визуализация символов шрифта, которые можно использовать.

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

После того, как холст с графиком отрисован, нам нужно создать другую панель статистики справа от холста с графиком. В этом случае мы хотим немного продвинуться вперед и смешать два цвета для фона в месте их соприкосновения с помощью линейной интерполяции, поскольку это довольно простая задача, а также затемнить цвета рамки на основе выбранных цветов фона, вместо статических цветов рамок, которые мы использовали до сих пор для заголовка и холста графика. Чтобы добиться этого с легкостью, нам понадобятся некоторые вспомогательные функции.
//+------------------------------------------------------------------+ //| 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. После компиляции получаем следующий результат.

На изображении видно, что мы создали панель статистики с цветовой интерполяцией. Если вам не нужна интерполяция, вы можете выполнить более жесткое смешивание границ, без плавных переходов, следующим образом: просто отбросьте математику.
//+------------------------------------------------------------------+ //| 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); }
При использовании этого подхода получаем следующий результат.

На изображении видно, что границы не совпадают линейно. Так что вам снова предстоит выбрать подход, который соответствует вашему стилю. После этого рендеринг нашей панели будет выполнен для режима темной темы оформления, которую мы выбрали по умолчанию. Чтобы включить взаимодействия на графике, нам нужно будет включить движения мыши во время инициализации. Теперь обработчик события инициализации в конечном счёте выглядит следующим образом.
//+------------------------------------------------------------------+ //| 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". Наконец, перерисовываем график, чтобы убедиться, что все остатки удалены с дисплея. После компиляции получаем следующий результат.

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

Заключение
В заключение отметим, что мы разработали панель цен на основе холста в MQL5, используя класс CCanvas для создания перетаскиваемых и изменяемых по размеру панелей для отображения графиков цен в реальном времени с графическим представлением линий, закрашенными областями и эффектами тумана, а также дополнительную панель статистики для показателей счета, таких как баланс/эквити и OHLC текущего бара, с поддержкой фоновых изображений, градиентов, переключения тем оформления и эффективной обработкой событий, обеспечивающей интерактивность. Система включает в себя бикубическое масштабирование для плавного изменения размера, альфа-смешивание для накладок и обновление по новым барам, обеспечивая настраиваемый инструмент для визуального мониторинга без использования встроенных объектов. В следующей части мы расширим функциональность панели, добавив текстовый экран на основе холста для чтения и прокрутки, способный интегрировать несколько текстовых тем с динамической полосой прокрутки, как те, что используются в новых модернизированных терминалах MetaQuotes. Следите за обновлениями!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21038
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Автоматизация торговых стратегий на MQL5 (Часть 24): Система торговли на пробое лондонской сессии с риск-менеджментом и трейлинг-стопами
Нейросети в трейдинге: Адаптивное масштабирование представлений (Окончание)
Машинное обучение и Data Science (Часть 42): Прогнозирование фондовых рынков с использованием N-BEATS в Python
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования