English
preview
Торговые инструменты на MQL5 (Часть 15): Эффекты размытия холста, рендеринг теней и плавная прокрутка колесом мыши

Торговые инструменты на MQL5 (Часть 15): Эффекты размытия холста, рендеринг теней и плавная прокрутка колесом мыши

MetaTrader 5Трейдинг |
41 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

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

В итоге у вас будет полнофункциональная панель MQL5 с визуально улучшенными и более интерактивными элементами. Она упростит настройку и улучшит уровень пользовательского опыта. Давайте перейдём к реализации!


Изучение эффекта размытия холста и рендеринга теней

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

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

BLUR AND SHADOW RENDERING ILLUSTRATION


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

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

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART3.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>

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

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

enum ENUM_RESIZE_MODE {                 // Define resize mode enumeration
   NONE,                                // No resize mode
   BOTTOM,                              // Bottom resize mode
   RIGHT,                               // Right resize mode
   BOTTOM_RIGHT                         // Bottom-right resize mode
};

//+------------------------------------------------------------------+
//| Canvas objects                                                   |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                    //--- Declare graph canvas object
CCanvas canvasStats;                    //--- Declare stats canvas object
CCanvas canvasHeader;                   //--- Declare header canvas object
CCanvas canvasText;                     //--- Declare text canvas object

//+------------------------------------------------------------------+
//| Canvas names                                                     |
//+------------------------------------------------------------------+
string canvasGraphName = "GraphCanvas"; //--- Set graph canvas name
string canvasStatsName = "StatsCanvas"; //--- Set stats canvas name
string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name
string canvasTextName = "TextCanvas";   //--- Set text canvas name

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "=== GENERAL CANVAS SETTINGS ==="
input int CanvasX                     = 30;                             // Canvas X Position
input int CanvasY                     = 50;                             // Canvas Y Position
input int CanvasWidth                 = 400;                            // Canvas Width
input int CanvasHeight                = 300;                            // Canvas Height
input bool EnableStatsPanel           = true;                           // Enable Stats Panel
input int PanelGap                    = 10;                             // Panel Gap
input bool EnableTextPanel            = true;                           // Enable Text Panel
input int TextPanelHeight             = 200;                            // Text Panel Height
input double TextBackgroundOpacityPercent = 85.0;                       // Text Background Opacity Percent

sinput group "=== HEADER CANVAS SETTINGS ==="
input color HeaderShadowColor         = clrDodgerBlue;                  // Header Shadow Color
input double HeaderShadowOpacityPercent = 70.0;                         // Header Shadow Opacity Percent
input int HeaderShadowDistance        = 4;                              // Header Shadow Distance
input int HeaderShadowBlurRadius      = 3;                              // Header Shadow Blur Radius

sinput group "=== GRAPH PANEL SETTINGS ==="
input int graphBars                   = 50;                             // Graph Bars
input color borderColor               = clrBlack;                       // Border Color
input color borderHoverColor          = clrRed;                         // Border Hover Color
input bool UseBackground              = true;                           // Use Background
input double FogOpacity               = 0.5;                            // Fog Opacity
input bool BlendFog                   = true;                           // Blend Fog

sinput group "=== STATS PANEL SETTINGS ==="
input int StatsFontSize               = 12;                             // Stats Font Size
input color StatsLabelColor           = clrDodgerBlue;                  // Stats Label Color
input color StatsValueColor           = clrWhite;                       // Stats Value Color
input color StatsHeaderColor          = clrDodgerBlue;                  // Stats Header Color
input int StatsHeaderFontSize         = 14;                             // Stats Header Font Size
input double BorderOpacityPercentReduction = 20.0;                      // Border Opacity Percent Reduction
input double BorderDarkenPercent      = 30.0;                           // Border Darken Percent
input double StatsHeaderBgOpacityPercent = 20.0;                        // Stats Header Bg Opacity Percent
input int StatsHeaderBgRadius         = 8;                              // Stats Header Bg Radius
input ENUM_BACKGROUND_MODE StatsBackgroundMode = GradientTwoColors;     // Stats Background Mode
input color TopColor                   = clrBlack;                      // Top Color
input color BottomColor                = clrRed;                        // Bottom Color
input double BackgroundOpacity         = 0.7;                           // Background Opacity

Новая организация теперь выглядит так, как показано выше, где в группе ввода настроек холста заголовка указываются параметры для теневых эффектов: "HeaderShadowColor" для настройки оттенка тени, "HeaderShadowOpacityPercent" для управления прозрачностью в процентах, "HeaderShadowDistance" для смещения от заголовка и "HeaderShadowBlurRadius" для определения мягкости размытия. Для большей наглядности мы выделили наиболее важные изменения. Теперь это дает нам следующее окно с упорядоченными входными параметрами.

NEW INPUTS WINDOW

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

//+------------------------------------------------------------------+
//| Draw header on canvas                                            |
//+------------------------------------------------------------------+
void DrawHeaderOnCanvas() {             // Render header elements
   canvasHeader.Erase(0);               //--- Clear header canvas

   int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra space
   int inner_header_width = canvasHeader.Width() - 2 * extra; //--- Compute inner width

   int header_left = extra;             //--- Set header left
   int header_top = extra;              //--- Set header top
   int header_right = header_left + inner_header_width - 1; //--- Set header right
   int header_bottom = header_top + header_height - 1; //--- Set header bottom

   if (HeaderShadowBlurRadius > 0 || HeaderShadowDistance > 0) { //--- Check shadow settings
      int offset_x = HeaderShadowDistance; //--- Set X offset
      int offset_y = HeaderShadowDistance; //--- Set Y offset
      int blur = HeaderShadowBlurRadius;   //--- Set blur radius

      for(int layer = blur; layer >= 0; layer--) {     //--- Loop through layers
         double factor = (double)layer / (blur + 1.0); //--- Compute factor
         uchar alpha = (uchar)(255 * (HeaderShadowOpacityPercent / 100.0) * (1.0 - factor)); //--- Compute alpha
         uint argb_shadow = ColorToARGB(HeaderShadowColor, alpha); //--- Convert to ARGB

         int s_left = header_left + offset_x - layer;     //--- Set shadow left
         int s_top = header_top + offset_y - layer;       //--- Set shadow top
         int s_right = header_right + offset_x + layer;   //--- Set shadow right
         int s_bottom = header_bottom + offset_y + layer; //--- Set shadow bottom

         int s_width = s_right - s_left + 1;  //--- Compute shadow width
         int s_height = s_bottom - s_top + 1; //--- Compute shadow height
         int s_radius = layer;                //--- Set shadow radius

         if (s_width > 0 && s_height > 0) {   //--- Check valid dimensions
            FillRoundedRectangle(canvasHeader, s_left, s_top, s_width, s_height, s_radius, argb_shadow); //--- Fill shadow rectangle
         }
      }
   }

   color header_bg = panel_dragging ? GetHeaderDragColor() : (header_hovered ? GetHeaderHoverColor() : GetHeaderColor()); //--- Determine bg color
   uint argb_bg = ColorToARGB(header_bg, 255); //--- Convert to ARGB
   canvasHeader.FillRectangle(header_left, header_top, header_right, header_bottom, argb_bg); //--- Fill header rectangle

   uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border to ARGB
   canvasHeader.Line(header_left, header_top, header_right, header_top, argbBorder);       //--- Draw top border
   canvasHeader.Line(header_right, header_top, header_right, header_bottom, argbBorder);   //--- Draw right border
   canvasHeader.Line(header_right, header_bottom, header_left, header_bottom, argbBorder); //--- Draw bottom border
   canvasHeader.Line(header_left, header_bottom, header_left, header_top, argbBorder);     //--- Draw left border

   canvasHeader.FontSet("Arial Bold", 15);                 //--- Set font for title
   uint argbText = ColorToARGB(GetHeaderTextColor(), 255); //--- Convert text to ARGB
   canvasHeader.TextOut(header_left + 10, header_top + (header_height - 15) / 2, "Price Dashboard", argbText, TA_LEFT); //--- Draw title text

   int theme_x = header_left + inner_header_width + theme_x_offset;      //--- Compute theme X
   string theme_symbol = CharToString((uchar)91);                        //--- Set theme symbol
   color theme_color = theme_hovered ? clrYellow : GetHeaderTextColor(); //--- Determine theme color
   canvasHeader.FontSet("Wingdings", 22);                                //--- Set font for theme
   uint argb_theme = ColorToARGB(theme_color, 255);                      //--- Convert to ARGB
   canvasHeader.TextOut(theme_x, header_top + (header_height - 22) / 2, theme_symbol, argb_theme, TA_CENTER); //--- Draw theme symbol

   int min_x = header_left + inner_header_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(); //--- Determine minimize color
   canvasHeader.FontSet("Wingdings", 22);                                 //--- Set font for minimize
   uint argb_min = ColorToARGB(min_color, 255);                           //--- Convert to ARGB
   canvasHeader.TextOut(min_x, header_top + (header_height - 22) / 2, min_symbol, argb_min, TA_CENTER); //--- Draw minimize symbol

   int close_x = header_left + inner_header_width + close_x_offset;       //--- Compute close X
   string close_symbol = CharToString((uchar)114);                        //--- Set close symbol
   color close_color = close_hovered ? clrRed : GetHeaderTextColor();     //--- Determine close color
   canvasHeader.FontSet("Webdings", 22);                                  //--- Set font for close
   uint argb_close = ColorToARGB(close_color, 255);                       //--- Convert to ARGB
   canvasHeader.TextOut(close_x, header_top + (header_height - 22) / 2, close_symbol, argb_close, TA_CENTER); //--- Draw close symbol

   canvasHeader.Update();                                                 //--- Update header canvas
}

Мы вносим улучшения в функцию "DrawHeaderOnCanvas", чтобы визуализировать заголовок панели с помощью визуальных совершенствований. Сначала мы очищаем холст с помощью "canvasHeader.Erase", чтобы начать с чистого листа. Мы вычисляем дополнительное пространство как сумму значений "HeaderShadowBlurRadius" и "HeaderShadowDistance", затем определяем ширину внутреннего заголовка, вычитая удвоенное дополнительное пространство из ширины холста. Заданы позиции для прямоугольника заголовка: слева и сверху - дополнительно, справа - точно так же, как слева плюс внутренняя ширина минус 1, а снизу - так же, как сверху плюс "header_height" минус 1.

Если либо радиус размытия, либо расстояние до тени положительное, мы создаем эффект тени, проходя циклом от радиуса размытия до 0. Для каждого слоя мы вычисляем коэффициент как слой деленный на размытие плюс 1, затем значение альфа-канала как 255, умноженное на процент непрозрачности тени, умноженное на 1 минус коэффициент. Мы преобразуем "HeaderShadowColor" в ARGB, используя этот альфа-канал для цвета тени. Положения прямоугольников тени корректируются с помощью смещения и слоя для обеспечения эффекта распределения: слева — как значение слева от заголовка плюс смещение минус слой, и аналогично для верхней, правой и нижней частей. Мы вычисляем ширину и высоту тени, устанавливаем радиус слоя и, если размеры положительны, заполняем закругленный прямоугольник с помощью "FillRoundedRectangle", используя цвет тени.

Цвет фона заголовка определяется условно: при перетаскивании используется цвет перетаскивания, при наведении курсора - цвет при наведении курсора, в противном случае используется стандартный цвет заголовка. Мы преобразуем его в ARGB с полной непрозрачностью и заполняем прямоугольник заголовка. Границы рисуются в виде линий вокруг заголовка с использованием цвета рамки, преобразованного в ARGB. Для текста мы устанавливаем шрифт "Arial Bold" с размером 15, преобразуем цвет текста заголовка в ARGB и выводим "Ценовую панель", выровненную по левому краю с отступом.

Иконки следующие: для темы при вычисленном значении X мы используем шрифт Wingdings размером 22, устанавливаем для символа значение равное 91, окрашиваем в желтый цвет при наведении курсора, иначе — в цвет текста заголовка, и отображаем по центру. Значок сворачивания в точке X использует Wingdings, символ 111, если панель свернута, либо 114, окрашивается в желтый цвет при наведении курсора, рисуется по центру. Значок закрытия в точке X использует Wingdings, символ 114, окрашивается в красный цвет при наведении курсора, рисуется по центру. Наконец, вызываем "canvasHeader.Update", чтобы обновить отображение, как мы делали в предыдущих частях. Далее мы изменим логику наведения курсора мыши на заголовок, чтобы учитывать тень, расширив определение попадания, включив область тени вокруг заголовка, что обеспечит точность наведения курсора мыши / кликов по увеличенному холсту, а также предотвратит ложные срабатывания вблизи краев из-за отступов тени, что улучшит удобство использования.

//+------------------------------------------------------------------+
//| Check mouse over header                                          |
//+------------------------------------------------------------------+
bool IsMouseOverHeader(int mouse_x, int mouse_y) {            // Detect header hover
   int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra space
   int inner_header_width = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute inner width
   int header_canvas_x = currentCanvasX - extra;              //--- Set canvas X
   int header_canvas_y = currentCanvasY - extra;              //--- Set canvas Y
   int header_canvas_w = inner_header_width + 2 * extra;      //--- Set canvas width
   int header_canvas_h = header_height + 2 * extra;           //--- Set canvas height

   if (mouse_x < header_canvas_x || mouse_x > header_canvas_x + header_canvas_w || mouse_y < header_canvas_y || mouse_y > header_canvas_y + header_canvas_h) return false; //--- Return false if outside

   int theme_left = header_canvas_x + extra + inner_header_width + theme_x_offset - button_size / 2; //--- Set theme left
   int theme_right = theme_left + button_size;                //--- Set theme right
   int theme_top = header_canvas_y + extra;                   //--- Set theme top
   int theme_bottom = theme_top + header_height;              //--- Set theme bottom
   if (mouse_x >= theme_left && mouse_x <= theme_right && mouse_y >= theme_top && mouse_y <= theme_bottom) return false; //--- Return false if over theme

   int min_left = header_canvas_x + extra + inner_header_width + minimize_x_offset - button_size / 2; //--- Set minimize left
   int min_right = min_left + button_size;                    //--- Set minimize right
   int min_top = header_canvas_y + extra;                     //--- Set minimize top
   int min_bottom = min_top + header_height;                  //--- Set minimize bottom
   if (mouse_x >= min_left && mouse_x <= min_right && mouse_y >= min_top && mouse_y <= min_bottom) return false; //--- Return false if over minimize

   int close_left = header_canvas_x + extra + inner_header_width + close_x_offset - button_size / 2; //--- Set close left
   int close_right = close_left + button_size;                //--- Set close right
   int close_top = header_canvas_y + extra;                   //--- Set close top
   int close_bottom = close_top + header_height;              //--- Set close bottom
   if (mouse_x >= close_left && mouse_x <= close_right && mouse_y >= close_top && mouse_y <= close_bottom) return false; //--- Return false if over close

   return true;                                               //--- Return true if over header
}

Здесь мы обновили функцию "IsMouseOverHeader", чтобы она точно определяла наведение курсора мыши на область заголовка, теперь с учетом дополнительного отступа, создаваемого эффектами тени. Для этого мы вычисляем "extra" как сумму "HeaderShadowBlurRadius" и "HeaderShadowDistance", а затем корректируем координаты холста заголовка: "header_canvas_x" как "currentCanvasX" минус "extra", "header_canvas_y" как "currentCanvasY" минус "extra", "header_canvas_w" как "inner_header_width" плюс двойное значение "extra", и "header_canvas_h" как "header_height" плюс двойное значение "extra".

Мы проверяем, не выходит ли положение мыши за пределы этих расширенных границ холста, и возвращаем значение false, если это так. Для областей кнопок мы вычисляем позиции типа "theme_left", используя "header_canvas_x" плюс "extra" плюс "inner_header_width" плюс "theme_x_offset" минус половина "button_size", и аналогично для правой, верхней и нижней частей, возвращая значение false, если курсор мыши находится над кнопками темы, сворачивания или закрытия, чтобы исключить их из общего эффекта наведения курсора на заголовок. Теперь нам нужно будет скорректировать изменение размера заголовка для добавления значения тени extra на переключатель сворачивания, чтобы тень корректно отображалась при показе/скрытии панелей, обеспечивая визуальную согласованность.

//+------------------------------------------------------------------+
//| Toggle minimize                                                  |
//+------------------------------------------------------------------+
void ToggleMinimize() {                 // Switch minimize state
   panels_minimized = !panels_minimized; //--- Invert minimized state
   if (panels_minimized) {              //--- Check minimized
      canvasGraph.Destroy();            //--- Destroy graph canvas
      graphCreated = false;             //--- Reset graph flag
      if (EnableStatsPanel) {           //--- Check stats enabled
         canvasStats.Destroy();         //--- Destroy stats canvas
         statsCreated = false;          //--- Reset stats flag
      }
      if (EnableTextPanel) {            //--- Check text enabled
         canvasText.Destroy();          //--- Destroy text canvas
         textCreated = false;           //--- Reset text flag
      }
   } else {                             //--- Handle maximized
      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"); //--- Log failure
      }
      graphCreated = true;              //--- Set graph flag
      UpdateGraphOnCanvas();            //--- Update graph
      if (EnableStatsPanel) {           //--- Check stats enabled
         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"); //--- Log failure
         }
         statsCreated = true;           //--- Set stats flag
         UpdateStatsOnCanvas();         //--- Update stats
      }
      if (EnableTextPanel) {            //--- Check text enabled
         int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y
         int inner_header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute width
         int text_width = inner_header_width; //--- Set text width
         int text_height = TextPanelHeight;   //--- Set text height
         if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, text_width, text_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Recreate text
            Print("Failed to recreate Text Canvas"); //--- Log failure
         }
         textCreated = true;            //--- Set text flag
         UpdateTextOnCanvas();          //--- Update text
      }
   }

   int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra space
   int inner_header_width = currentWidth + (EnableStatsPanel && !panels_minimized ? PanelGap + currentWidth / 2 : 0); //--- Compute inner width
   int header_canvas_width = inner_header_width + 2 * extra; //--- Compute header width
   canvasHeader.Resize(header_canvas_width, header_height + 2 * extra); //--- Resize header
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, header_canvas_width); //--- Set X size
   ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height + 2 * extra); //--- Set Y size
   DrawHeaderOnCanvas();                //--- Redraw header
   canvasHeader.Update();               //--- Update header
   ChartRedraw();                       //--- Redraw chart
}

Мы обновили функцию "ToggleMinimize", чтобы она обрабатывала переключение между свернутым и развернутым состояниями панелей дашборда. В этой функции мы инвертируем значение параметра "panels_minimized", чтобы переключать состояние. При сворачивании окна мы уничтожаем холст графика с помощью метода "canvasGraph.Destroy" и сбрасываем значение параметра "graphCreated" в значение false; если включена статистика, мы уничтожаем объект "canvasStats" и сбрасываем "statsCreated"; если включен текст, мы уничтожаем объект "canvasText" и сбрасываем "textCreated".

При разворачивании мы создаем холст графика заново, используя метод "canvasGraph.CreateBitmapLabel" в текущей позиции и размере, устанавливаем значение параметра "graphCreated" в true и обновляем его с помощью метода "UpdateGraphOnCanvas". Если статистика включена, мы вычисляем позицию по оси X, создаем заново "canvasStats", устанавливаем "statsCreated" в значение true и обновляем данные с помощью "UpdateStatsOnCanvas". Если текст включен, мы вычисляем позицию по оси Y, определяем ширину как "inner_header_width", включая статистику, если она присутствует, вновь создаем "canvasText" с этой шириной и "TextPanelHeight", устанавливаем "textCreated" в значение true и обновляем с помощью "UpdateTextOnCanvas".

Затем мы корректируем размер заголовка: вычисляем "extra" как радиус размытия плюс расстояние тени, "inner_header_width" на основе текущей ширины и статистики, если заголовок не свернут, а затем "header_canvas_width" как значение inner плюс удвоенное значение extra. Мы изменяем размер "canvasHeader" до этой ширины и "header_height" плюс двойное значение extra, устанавливаем свойства объекта для размеров по осям X и Y, перерисовываем заголовок с помощью "DrawHeaderOnCanvas", обновляем его и перерисовываем график.

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

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

   //--- existing logic
   
   int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra space
   int inner_header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute inner width
   int header_canvas_width = inner_header_width + 2 * extra; //--- Compute header width
   int header_canvas_height = header_height + 2 * extra; //--- Compute header height

   if (!canvasHeader.CreateBitmapLabel(0, 0, canvasHeaderName, currentCanvasX - extra, currentCanvasY - extra, header_canvas_width, header_canvas_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create header canvas
      Print("Failed to create Header Canvas"); //--- Log creation failure
      return(INIT_FAILED);              //--- Return initialization failure
   }
   
   //--- existing logic
   
   return(INIT_SUCCEEDED);              //--- Return initialization success
}

Здесь мы улучшаем обработчик OnInit для включения эффектов тени на холсте заголовка во время инициализации. После установки текущих размеров и позиций мы вычисляем параметр "extra" как сумму «HeaderShadowBlurRadius» и «HeaderShadowDistance», чтобы обеспечить отступ для теней. Мы вычисляем "inner_header_width" как "currentWidth" плюс дополнительные значения панели статистики, если они включены, затем устанавливаем "header_canvas_width" равным внутренней ширине плюс удвоенное значение extra, а "header_canvas_height" равным "header_height" плюс удвоенное значение extra для полного охвата тени. Мы создаём холст заголовка с помощью "canvasHeader.CreateBitmapLabel" со смещением (текущие координаты X и Y минус extra), используя увеличенные ширину и высоту, а также нормализованный цветовой формат ARGB. Если создание не удается, мы выводим ошибку и возвращаем INIT_FAILED; в противном случае мы продолжаем использовать существующую логику и возвращаем INIT_SUCCEEDED в случае успеха. После компиляции получаем следующий результат.

CANVAS HEADER SHADOW

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

//+------------------------------------------------------------------+
//| Handle chart events                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { // Process chart events

   //--- existing logic
   
   } else if (id == CHARTEVENT_MOUSE_WHEEL) { //--- Check mouse wheel
      int flg_keys = (int)(lparam >> 32);     //--- Get keys
      int mx = (int)(short)lparam;            //--- Get X
      int my = (int)(short)(lparam >> 16);    //--- Get Y
      int delta = (int)dparam;                //--- Get delta

      if (EnableTextPanel && !panels_minimized && text_scroll_visible) {                  //--- Check text wheel
         int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
         int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE);     //--- Get width
         int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE);     //--- Get height
         bool is_over_text_body = (mx >= text_canvas_x && mx <= text_canvas_x + text_canvas_w - text_track_width &&
                                   my >= text_canvas_y && my <= text_canvas_y + text_canvas_h); //--- Check over body
         if (is_over_text_body) {            //--- Handle over body
            int scroll_step = 20;            //--- Set step
            text_scroll_pos += (delta > 0 ? -scroll_step : scroll_step);                  //--- Adjust pos
            text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll));      //--- Clamp pos
            UpdateTextOnCanvas();            //--- Update text

            // REMOVED: Scale revert code (this was causing the chart scale interference)
            // No need to change CHART_SCALE; wheel now only scrolls text content.
            
            int current_scale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get scale
            int adjust = (delta > 0 ? 1 : -1);  // Swap to (delta > 0 ? -1 : 1) if wheel direction is opposite
            int revert_scale = current_scale + adjust; //--- Calculate revert
            revert_scale = MathMax(0, MathMin(5, revert_scale)); //--- Clamp scale
            ChartSetInteger(0, CHART_SCALE, revert_scale); //--- Set scale

            ChartRedraw();                  //--- Redraw chart
         }
      }
   }
}

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

//+------------------------------------------------------------------+
//| Handle chart events                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { // Process chart events
   if (id == CHARTEVENT_CHART_CHANGE) { //--- Check chart change
      DrawHeaderOnCanvas();             //--- Redraw header
      UpdateGraphOnCanvas();            //--- Update graph
      if (EnableStatsPanel) UpdateStatsOnCanvas(); //--- Update stats
      if (EnableTextPanel) UpdateTextOnCanvas();   //--- Update text
      ChartRedraw();                    //--- Redraw chart
   } else if (id == CHARTEVENT_MOUSE_MOVE) { //--- Check 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 prev header hover
      bool prev_min_hovered = minimize_hovered;  //--- Store prev minimize hover
      bool prev_close_hovered = close_hovered;   //--- Store prev close hover
      bool prev_theme_hovered = theme_hovered;   //--- Store prev theme hover
      bool prev_resize_hovered = resize_hovered; //--- Store prev resize hover

      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 hover change

      if (hover_changed) {              //--- Handle change
         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); //--- Apply header tooltip

      string resize_tooltip = "";       //--- Initialize resize tooltip
      if (resize_hovered || resizing) { //--- Check resize state
         ENUM_RESIZE_MODE active_mode = resizing ? resize_mode : hover_mode; //--- Get mode
         switch (active_mode) {         //--- Switch mode
         case BOTTOM:                   //--- Handle bottom
            resize_tooltip = "Resize Bottom"; //--- Set tooltip
            break;                      //--- Exit case
         case RIGHT:                    //--- Handle right
            resize_tooltip = "Resize Right"; //--- Set tooltip
            break;                      //--- Exit case
         case BOTTOM_RIGHT:             //--- Handle bottom-right
            resize_tooltip = "Resize Bottom-Right"; //--- Set tooltip
            break;                      //--- Exit case
         default:                       //--- Handle default
            break;                      //--- Exit case
         }
      }
      ObjectSetString(0, canvasGraphName, OBJPROP_TOOLTIP, resize_tooltip); //--- Apply graph tooltip

      if (EnableTextPanel && !panels_minimized) { //--- Check text enabled
         int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
         int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE);     //--- Get text width
         int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE);     //--- Get text height
         bool is_over_text = (mouse_x >= text_canvas_x && mouse_x <= text_canvas_x + text_canvas_w &&
                              mouse_y >= text_canvas_y && mouse_y <= text_canvas_y + text_canvas_h); //--- Check over text
         bool prev_scroll_hovered = text_scroll_area_hovered; //--- Store prev scroll hover
         text_scroll_area_hovered = false;         //--- Reset area hover
         if (is_over_text) {                       //--- Handle over text
            int local_x = mouse_x - text_canvas_x; //--- Compute local X
            int local_y = mouse_y - text_canvas_y; //--- Compute local Y
            if (local_x >= text_canvas_w - text_track_width) { //--- Check in scroll area
               text_scroll_area_hovered = true;    //--- Set area hover
            }
            bool prev_up = text_scroll_up_hovered; //--- Store prev up
            bool prev_down = text_scroll_down_hovered;     //--- Store prev down
            bool prev_slider = text_scroll_slider_hovered; //--- Store prev slider
            TextUpdateHoverEffects(local_x, local_y);      //--- Update hovers
            if (prev_scroll_hovered != text_scroll_area_hovered || prev_up != text_scroll_up_hovered || prev_down != text_scroll_down_hovered || prev_slider != text_scroll_slider_hovered) { //--- Check change
               UpdateTextOnCanvas();              //--- Update text
               ChartRedraw();                     //--- Redraw chart
            }
            text_mouse_in_body = (local_x < text_canvas_w - text_track_width); //--- Set body mouse
         } else {                                 //--- Handle not over text
            bool need_redraw = prev_scroll_hovered || text_scroll_up_hovered || text_scroll_down_hovered || text_scroll_slider_hovered; //--- Check need redraw
            if (need_redraw) {                    //--- Handle redraw
               text_scroll_area_hovered = false;  //--- Reset area
               text_scroll_up_hovered = false;    //--- Reset up
               text_scroll_down_hovered = false;  //--- Reset down
               text_scroll_slider_hovered = false; //--- Reset slider
               UpdateTextOnCanvas();              //--- Update text
               ChartRedraw();                     //--- Redraw chart
            }
            text_mouse_in_body = false;           //--- Reset body mouse
         }
         if (text_mouse_in_body != prev_text_mouse_in_body) { //--- Check body change
            ChartSetInteger(0, CHART_MOUSE_SCROLL, !text_mouse_in_body); //--- Set mouse scroll
            prev_text_mouse_in_body = text_mouse_in_body; //--- Update prev
         }
      }

      if (mouse_state == 1 && prev_mouse_state == 0) { //--- Check mouse down
         if (header_hovered) {          //--- Handle header click
            panel_dragging = true;      //--- Set dragging
            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();       //--- Redraw header
            ChartRedraw();              //--- Redraw chart
         } else if (theme_hovered) {    //--- Handle theme click
            ToggleTheme();              //--- Toggle theme
         } else if (minimize_hovered) { //--- Handle minimize click
            ToggleMinimize();           //--- Toggle minimize
         } else if (close_hovered) {    //--- Handle close click
            CloseDashboard();           //--- Close dashboard
         } else {                       //--- Handle other clicks
            ENUM_RESIZE_MODE temp_mode = NONE; //--- Set temp mode
            if (!panel_dragging && !resizing && IsMouseOverResize(mouse_x, mouse_y, temp_mode)) { //--- Check resize start
               resizing = true;         //--- Set 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();   //--- Update graph
               ChartRedraw();           //--- Redraw chart
            }
         }
         if (EnableTextPanel && !panels_minimized && text_scroll_visible && text_scroll_area_hovered) { //--- Check text click
            int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE);            //--- Get text X
            int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE);            //--- Get text Y
            int local_x = mouse_x - text_canvas_x;                                                      //--- Compute local X
            int local_y = mouse_y - text_canvas_y;                                                      //--- Compute local Y
            int scrollbar_x = canvasText.Width() - text_track_width;                                    //--- Set scrollbar X
            int scrollbar_y = 0;                                                                        //--- Set scrollbar Y
            int scrollbar_height = canvasText.Height();                                                 //--- Set height
            int scroll_area_y = scrollbar_y + text_button_size;                                         //--- Set area Y
            int scroll_area_height = scrollbar_height - 2 * text_button_size;                           //--- Compute area height
            int slider_y = scroll_area_y + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Compute slider Y
            if (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1) {              //--- Check in scrollbar
               if (local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1) {           //--- Check up button
                  TextScrollUp();                                                                       //--- Scroll up
               } else if (local_y >= scrollbar_y + scrollbar_height - text_button_size && local_y <= scrollbar_y + scrollbar_height - 1) { //--- Check down button
                  TextScrollDown();                                                                     //--- Scroll down
               } else if (local_y >= scroll_area_y && local_y <= scroll_area_y + scroll_area_height - 1) { //--- Check slider area
                  if (local_y >= slider_y && local_y <= slider_y + text_slider_height - 1) {            //--- Check on slider
                     text_movingStateSlider = true;                                                     //--- Set moving state
                     text_mlbDownY_Slider = local_y;                                                    //--- Set down Y
                     text_mlbDown_YD_Slider = slider_y;                                                 //--- Set down YD
                     ChartSetInteger(0, CHART_MOUSE_SCROLL, false);                                     //--- Disable scroll
                  } else {                                                                              //--- Handle track click
                     int new_slider_y = local_y - text_slider_height / 2;                               //--- Compute new Y
                     new_slider_y = MathMax(scroll_area_y, MathMin(new_slider_y, scroll_area_y + scroll_area_height - text_slider_height)); //--- Clamp Y
                     double ratio = (double)(new_slider_y - scroll_area_y) / (scroll_area_height - text_slider_height); //--- Compute ratio
                     text_scroll_pos = (int)MathRound(ratio * text_max_scroll);                         //--- Set scroll pos
                  }
                  UpdateTextOnCanvas();   //--- Update text
                  ChartRedraw();          //--- Redraw chart
               }
            }
         }
      } else if (panel_dragging && mouse_state == 1) { //--- Check dragging
         int dx = mouse_x - panel_drag_x; //--- Compute delta X
         int dy = mouse_y - panel_drag_y; //--- Compute delta Y
         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) + (EnableTextPanel && !panels_minimized ? PanelGap + TextPanelHeight : 0); //--- Compute full height
         new_x = MathMax(0, MathMin(chart_w - full_w, new_x)); //--- Clamp new X
         new_y = MathMax(0, MathMin(chart_h - full_h, new_y)); //--- Clamp new Y

         currentCanvasX = new_x;         //--- Update canvas X
         currentCanvasY = new_y;         //--- Update canvas Y

         int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_XDISTANCE, new_x - extra); //--- Set header X
         ObjectSetInteger(0, canvasHeaderName, OBJPROP_YDISTANCE, new_y - extra); //--- Set header Y
         if (!panels_minimized) {       //--- Check not minimized
            ObjectSetInteger(0, canvasGraphName, OBJPROP_XDISTANCE, new_x); //--- Set graph X
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Set graph Y
            if (EnableStatsPanel) {     //--- Check stats enabled
               int stats_x = new_x + currentWidth + PanelGap; //--- Compute stats X
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, stats_x); //--- Set stats X
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YDISTANCE, new_y + header_height + gap_y); //--- Set stats Y
            }
            if (EnableTextPanel) {      //--- Check text enabled
               int textY = new_y + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y
               ObjectSetInteger(0, canvasTextName, OBJPROP_XDISTANCE, new_x); //--- Set text X
               ObjectSetInteger(0, canvasTextName, OBJPROP_YDISTANCE, textY); //--- Set text Y
            }
         }
         ChartRedraw();                 //--- Redraw chart
      } else if (resizing && mouse_state == 1) { //--- Check resizing
         int dx = mouse_x - resize_start_x; //--- Compute delta X
         int dy = mouse_y - resize_start_y; //--- Compute delta Y
         int new_width = currentWidth;  //--- Set new width
         int new_height = currentHeight; //--- Set new height
         if (resize_mode == RIGHT || resize_mode == BOTTOM_RIGHT) { //--- Check right modes
            new_width = MathMax(min_width, start_width + dx); //--- Adjust width
         }
         if (resize_mode == BOTTOM || resize_mode == BOTTOM_RIGHT) { //--- Check bottom modes
            new_height = MathMax(min_height, start_height + dy); //--- Adjust height
         }

         if (new_width != currentWidth || new_height != currentHeight) { //--- Check dimension 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 - (EnableTextPanel ? PanelGap + TextPanelHeight : 0)); //--- Clamp height
            if (EnableStatsPanel) {     //--- Check stats enabled
               double max_w_d = (avail_w - PanelGap) / 1.5; //--- Compute max width double
               int max_w = (int)MathFloor(max_w_d); //--- Floor max width
               new_width = MathMin(new_width, max_w); //--- Clamp width
            } else {                    //--- Handle 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 pixels
               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 pixels
                  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); //--- Set graph X size
            ObjectSetInteger(0, canvasGraphName, OBJPROP_YSIZE, currentHeight); //--- Set graph Y size

            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); //--- Set stats X size
               ObjectSetInteger(0, canvasStatsName, OBJPROP_YSIZE, currentHeight); //--- Set stats Y size
               int stats_x = currentCanvasX + currentWidth + PanelGap; //--- Compute stats X
               ObjectSetInteger(0, canvasStatsName, OBJPROP_XDISTANCE, stats_x); //--- Set stats X distance
            }

            int extra = HeaderShadowBlurRadius + HeaderShadowDistance; //--- Compute extra
            int inner_header_width = currentWidth + (EnableStatsPanel ? PanelGap + currentWidth / 2 : 0); //--- Compute inner width
            int header_canvas_width = inner_header_width + 2 * extra; //--- Compute header width
            canvasHeader.Resize(header_canvas_width, header_height + 2 * extra); //--- Resize header
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_XSIZE, header_canvas_width); //--- Set header X size
            ObjectSetInteger(0, canvasHeaderName, OBJPROP_YSIZE, header_height + 2 * extra); //--- Set header Y size
            if (EnableTextPanel) {     //--- Check text
               int text_width = inner_header_width; //--- Set text width
               canvasText.Resize(text_width, TextPanelHeight); //--- Resize text
               ObjectSetInteger(0, canvasTextName, OBJPROP_XSIZE, text_width); //--- Set text X size
               ObjectSetInteger(0, canvasTextName, OBJPROP_YSIZE, TextPanelHeight); //--- Set text Y size
               int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y
               ObjectSetInteger(0, canvasTextName, OBJPROP_YDISTANCE, textY); //--- Set text Y distance
            }

            DrawHeaderOnCanvas();      //--- Redraw header
            UpdateGraphOnCanvas();     //--- Update graph
            if (EnableStatsPanel) UpdateStatsOnCanvas(); //--- Update stats
            if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text
            ChartRedraw();             //--- Redraw chart
         }
      } else if (text_movingStateSlider && mouse_state == 1) { //--- Check slider moving
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
         int local_y = mouse_y - text_canvas_y; //--- Compute local Y
         int delta_y = local_y - text_mlbDownY_Slider; //--- Compute delta Y
         int new_slider_y = text_mlbDown_YD_Slider + delta_y; //--- Compute new Y
         int scrollbar_y = 0;       //--- Set scrollbar Y
         int scrollbar_height = canvasText.Height(); //--- Set height
         int slider_min_y = scrollbar_y + text_button_size; //--- Set min Y
         int slider_max_y = scrollbar_y + scrollbar_height - text_button_size - text_slider_height; //--- Set max Y
         new_slider_y = MathMax(slider_min_y, MathMin(new_slider_y, slider_max_y)); //--- Clamp Y
         double scroll_ratio = (double)(new_slider_y - slider_min_y) / (slider_max_y - slider_min_y); //--- Compute ratio
         int new_scroll_pos = (int)MathRound(scroll_ratio * text_max_scroll); //--- Compute new pos
         if (new_scroll_pos != text_scroll_pos) { //--- Check change
            text_scroll_pos = new_scroll_pos; //--- Update pos
            UpdateTextOnCanvas();      //--- Update text
            ChartRedraw();             //--- Redraw chart
         }
      } else if (mouse_state == 0 && prev_mouse_state == 1) { //--- Check mouse up
         if (panel_dragging) {          //--- Handle drag end
            panel_dragging = false;     //--- Reset dragging
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll
            DrawHeaderOnCanvas();       //--- Redraw header
            ChartRedraw();              //--- Redraw chart
         }
         if (resizing) {                //--- Handle resize end
            resizing = false;           //--- Reset resizing
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll
            UpdateGraphOnCanvas();      //--- Update graph
            ChartRedraw();              //--- Redraw chart
         }
         if (text_movingStateSlider) {  //--- Handle slider end
            text_movingStateSlider = false; //--- Reset moving
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll
            UpdateTextOnCanvas();       //--- Update text
            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 prev state
   } else if (id == CHARTEVENT_MOUSE_WHEEL) { //--- Check mouse wheel
      int flg_keys = (int)(lparam >> 32); //--- Get keys
      int mx = (int)(short)lparam;      //--- Get X
      int my = (int)(short)(lparam >> 16); //--- Get Y
      int delta = (int)dparam;          //--- Get delta

      if (EnableTextPanel && !panels_minimized && text_scroll_visible) { //--- Check text wheel
         int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X
         int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y
         int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE);     //--- Get width
         int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE);     //--- Get height
         bool is_over_text_body = (mx >= text_canvas_x && mx <= text_canvas_x + text_canvas_w - text_track_width &&
                                   my >= text_canvas_y && my <= text_canvas_y + text_canvas_h); //--- Check over body
         if (is_over_text_body) {       //--- Handle over body
            int scroll_step = 20;       //--- Set step
            text_scroll_pos += (delta > 0 ? -scroll_step : scroll_step); //--- Adjust pos
            text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp pos
            UpdateTextOnCanvas();       //--- Update text

            // REMOVED: Scale revert code (this was causing the chart scale interference)
            // No need to change CHART_SCALE; wheel now only scrolls text content.

            ChartRedraw();              //--- Redraw chart
         }
      }
   }
}

Изначально именно таким было поведение колесика прокрутки.

BEFORE SMOOTH WHEEL SCROLL HANDLING

После внесения изменений мы получаем следующее.

AFTER SMOOTH WHEEL SCROLL HANDLING

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


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

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

BACKTEST GIF


Заключение

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

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

Прикрепленные файлы |
Внедрение в MQL5 практических модулей из других языков (Часть 04): Модули time, date и datetime из Python Внедрение в MQL5 практических модулей из других языков (Часть 04): Модули time, date и datetime из Python
В отличие от MQL5, язык программирования Python предлагает контроль и гибкость, когда речь заходит о работе со временем и управлении им. В этой статье мы реализуем модули, аналогичные модулям в языке MQL5 для более удобной обработки дат и времени, как в Python.
Эволюционный отбор LLM-агентов в MetaTrader 5 Эволюционный отбор LLM-агентов в MetaTrader 5
Статья описывает архитектуру торговой системы из 20 LLM-агентов на базе Grok (xAI), каждый из которых несёт уникальную торговую философию — от чистого моментума до статистического z-score. Система применяет генетический алгоритм прямо в ходе торговли: каждые 20 сделок автоматически убивает слабых агентов, клонирует сильных с мутацией промпта и публикует лидерборд на графике MetaTrader 5 — без остановки торговли и без единого SDK.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Торговые инструменты на MQL5 (Часть 14): Прокручиваемый текстовый холст с пиксельной точностью, сглаживанием и закругленной полосой прокрутки Торговые инструменты на MQL5 (Часть 14): Прокручиваемый текстовый холст с пиксельной точностью, сглаживанием и закругленной полосой прокрутки
В этой статье мы улучшим ценовую панель на основе холста в MQL5, добавляя прокручиваемую текстовую панель с пиксельной точностью для руководств по использованию, преодолевающую собственные ограничения на прокрутку за счет настраиваемого сглаживания и округлого дизайна полосы прокрутки с функцией расширения при наведении курсора. Текстовая панель поддерживает фоны темы оформления с непрозрачностью, динамический перенос строк для содержимого, такого как инструкции и контакты, и интерактивную навигацию с помощью кнопок вверх / вниз, перетаскивания ползунка и прокрутки колесика мыши в области основного текста.