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

Торговые инструменты на MQL5 (Часть 31): Создание интерактивной палитры инструментов в MQL5

MetaTrader 5Трейдинг |
55 4
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

У вас уже есть отлично отображаемая "Палитра инструментов" на графике MT5 — чистая компоновка, сглаженные значки и две темы — но это всего лишь визуальная оболочка: щелчки ничего не делают. Недостающим элементом является слой взаимодействия, который превращает отрисовку в поведение: надежная проверка попадания по bitmap label-объектам, обработчик событий графика, который обрабатывает события CHARTEVENT_*, а также детерминированный конечный автомат, который сопоставляет ввод пользователя с выбором инструмента, манипуляцией панелью и размещением объектов.

Эта статья написана для разработчиков MetaQuotes Language 5(MQL5) и алгоритмических трейдеров, которым нужен проверяемый, готовый к промышленному использованию интерактивный слой поверх пользовательского интерфейса. Наша цель конкретна: реализовать единую точку входа "OnEvent"/"OnChartEvent" и вспомогательные классы, обеспечивающие следующие минимальные сценарии использования:

  • Выбор инструмента и размещение объекта графика
  • Прокрутка переполненных категорий
  • Перетаскивание панели и привязка её к краям
  • Изменение размера панели снизу
  • Смена темы в реальном времени

Для этого мы расширяем архитектуру из Части 30 до десяти взаимодействующих классов: реестр инструментов, слои холста (включая холсты выдвижной панели в высоком разрешении), компоновка и проверка попаданий, менеджер всплывающих окон, рендерер, маршрутизатор событий и механизм рисования, поддерживающий размещение в один, два и три клика. На протяжении всей статьи мы рассматриваем контракт взаимодействия (activeTool, clicksRequired, последовательность размещения) как единственный источник истины, поэтому поведение является тестируемым и расширяемым. Мы рассмотрим следующие темы:

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

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


От статической боковой панели к интерактивной системе рисования

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

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

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

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

TOOLS PALETTE PART 3 ARCHITECTURE GIF


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

Расширение определений значков, перечислений, входных данных и структур

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

//+------------------------------------------------------------------+
//|                                         Tools Palette Part 3.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


//--- Define icon for each individual drawing tool using font/char pairs
SIconDefinition ICON_TOOL_POINTER         = { "Wingdings 3", (uchar)'-'  }; // Pointer tool icon
SIconDefinition ICON_TOOL_CROSSHAIR       = { "Wingdings",   (uchar)'W'  }; // Crosshair tool icon
SIconDefinition ICON_TOOL_TRENDLINE       = { "Wingdings 3", (uchar)'&'  }; // Trend line tool icon
SIconDefinition ICON_TOOL_HLINE           = { "Wingdings 3", (uchar)'"'  }; // Horizontal line tool icon
SIconDefinition ICON_TOOL_VLINE           = { "Wingdings 3", (uchar)'#'  }; // Vertical line tool icon
SIconDefinition ICON_TOOL_RAY             = { "Wingdings 3", (uchar)'&'  }; // Ray line tool icon
SIconDefinition ICON_TOOL_EXTENDED_LINE   = { "Wingdings 3", (uchar)'1'  }; // Extended line tool icon
SIconDefinition ICON_TOOL_INFO_LINE       = { "Wingdings 3", (uchar)'2'  }; // Info/measure line tool icon
SIconDefinition ICON_TOOL_PARALLEL_CH     = { "Wingdings 3", (uchar)'H'  }; // Parallel channel tool icon
SIconDefinition ICON_TOOL_REGRESSION_CH   = { "Wingdings 3", (uchar)'I'  }; // Regression channel tool icon
SIconDefinition ICON_TOOL_STDDEV_CH       = { "Wingdings 3", (uchar)'J'  }; // Standard deviation channel tool icon
SIconDefinition ICON_TOOL_PITCHFORK       = { "Wingdings 3", (uchar)'H'  }; // Andrew's pitchfork tool icon
SIconDefinition ICON_TOOL_SCHIFF          = { "Wingdings 3", (uchar)'I'  }; // Schiff pitchfork tool icon
SIconDefinition ICON_TOOL_MOD_SCHIFF      = { "Wingdings 3", (uchar)'K'  }; // Modified Schiff pitchfork tool icon
SIconDefinition ICON_TOOL_GANN_LINE       = { "Wingdings 3", (uchar)'&'  }; // Gann line tool icon
SIconDefinition ICON_TOOL_GANN_FAN        = { "Wingdings 3", (uchar)'0'  }; // Gann fan tool icon
SIconDefinition ICON_TOOL_GANN_GRID       = { "Wingdings",   (uchar)'i'  }; // Gann grid tool icon
SIconDefinition ICON_TOOL_FIBO_RET        = { "Wingdings",   (uchar)'['  }; // Fibonacci retracement tool icon
SIconDefinition ICON_TOOL_FIBO_EXP        = { "Wingdings 3", (uchar)'&'  }; // Fibonacci expansion tool icon
SIconDefinition ICON_TOOL_FIBO_CH         = { "Wingdings 3", (uchar)'H'  }; // Fibonacci channel tool icon
SIconDefinition ICON_TOOL_FIBO_TZ         = { "Wingdings 3", (uchar)'#'  }; // Fibonacci time zones tool icon
SIconDefinition ICON_TOOL_FIBO_FAN        = { "Wingdings 3", (uchar)'J'  }; // Fibonacci fan tool icon
SIconDefinition ICON_TOOL_FIBO_ARCS       = { "Wingdings",   (uchar)'l'  }; // Fibonacci arcs tool icon
SIconDefinition ICON_TOOL_RECTANGLE       = { "Wingdings",   (uchar)'o'  }; // Rectangle tool icon
SIconDefinition ICON_TOOL_TRIANGLE        = { "Wingdings 3", (uchar)'p'  }; // Triangle tool icon
SIconDefinition ICON_TOOL_ELLIPSE         = { "Wingdings",   (uchar)'l'  }; // Ellipse tool icon
SIconDefinition ICON_TOOL_TEXT            = { "Webdings",    (uchar)'>'  }; // Text label tool icon
SIconDefinition ICON_TOOL_ARROW_UP        = { "Wingdings",   (uchar)225  }; // Arrow up tool icon
SIconDefinition ICON_TOOL_ARROW_DOWN      = { "Wingdings",   (uchar)226  }; // Arrow down tool icon
SIconDefinition ICON_TOOL_THUMB_UP        = { "Wingdings",   (uchar)'C'  }; // Thumbs up tool icon
SIconDefinition ICON_TOOL_THUMB_DOWN      = { "Wingdings",   (uchar)'D'  }; // Thumbs down tool icon
SIconDefinition ICON_TOOL_PRICE_LABEL     = { "Wingdings",   (uchar)234  }; // Left price label tool icon
SIconDefinition ICON_TOOL_STOP_SIGN       = { "Wingdings",   (uchar)251  }; // Stop sign tool icon
SIconDefinition ICON_TOOL_CHECK_MARK      = { "Wingdings",   (uchar)252  }; // Check mark tool icon

enum TOOL_TYPE
  {
   TOOL_NONE = 0,          // No tool active
   TOOL_POINTER,           // Default pointer cursor
   TOOL_CROSSHAIR,         // Crosshair / measure cursor
   TOOL_TRENDLINE,         // Trend line drawing tool
   TOOL_HLINE,             // Horizontal line drawing tool
   TOOL_VLINE,             // Vertical line drawing tool
   TOOL_RAY,               // Ray line drawing tool
   TOOL_EXTENDED_LINE,     // Extended (infinite) line drawing tool
   TOOL_INFO_LINE,         // Info / measure line drawing tool
   TOOL_PARALLEL_CHANNEL,  // Parallel channel drawing tool
   TOOL_REGRESSION_CHANNEL,// Regression channel drawing tool
   TOOL_STDDEV_CHANNEL,    // Standard deviation channel drawing tool
   TOOL_PITCHFORK,         // Andrew's pitchfork drawing tool
   TOOL_SCHIFF_PITCHFORK,  // Schiff pitchfork drawing tool
   TOOL_MOD_SCHIFF,        // Modified Schiff pitchfork drawing tool
   TOOL_GANN_LINE,         // Gann line drawing tool
   TOOL_GANN_FAN,          // Gann fan drawing tool
   TOOL_GANN_GRID,         // Gann grid drawing tool
   TOOL_FIBO_RETRACEMENT,  // Fibonacci retracement drawing tool
   TOOL_FIBO_EXPANSION,    // Fibonacci expansion drawing tool
   TOOL_FIBO_CHANNEL,      // Fibonacci channel drawing tool
   TOOL_FIBO_TIMEZONES,    // Fibonacci time zones drawing tool
   TOOL_FIBO_FAN,          // Fibonacci fan drawing tool
   TOOL_FIBO_ARCS,         // Fibonacci arcs drawing tool
   TOOL_RECTANGLE,         // Rectangle shape drawing tool
   TOOL_TRIANGLE,          // Triangle shape drawing tool
   TOOL_ELLIPSE,           // Ellipse shape drawing tool
   TOOL_TEXT,              // Text label annotation tool
   TOOL_ARROW_UP,          // Arrow up annotation tool
   TOOL_ARROW_DOWN,        // Arrow down annotation tool
   TOOL_THUMB_UP,          // Thumbs up annotation tool
   TOOL_THUMB_DOWN,        // Thumbs down annotation tool
   TOOL_PRICE_LABEL,       // Left price label annotation tool
   TOOL_STOP_SIGN,         // Stop sign annotation tool
   TOOL_CHECK_MARK         // Check mark annotation tool
  };

input int    FlyoutIconSize    = 22;  // Flyout Icon Size (pt)
input int    FlyoutLabelSize   = 15;  // Flyout Label Font Size (pt)
input int    FlyoutTitleSize   = 14;  // Flyout Title Font Size (pt)
input int    MouseScrollSpeed  = 8;   // Mouse Scroll Step (px)

//+------------------------------------------------------------------+
//| Tool definition structure                                        |
//+------------------------------------------------------------------+
struct ToolDefinition
  {
   TOOL_TYPE toolType;    // Unique tool type identifier
   string    toolLabel;   // Display label shown in the flyout panel
   string    iconFontName;// Font name used to render the tool icon
   uchar     iconCharCode;// Character code of the tool icon glyph
   string    tooltipText; // Tooltip string shown on hover
  };

//+------------------------------------------------------------------+
//| Category definition structure                                    |
//+------------------------------------------------------------------+
struct CategoryDefinition
  {
   string         categoryLabel; // Display label for the category
   string         iconFontName;  // Font name used to render the category icon
   uchar          iconCharCode;  // Character code of the category icon glyph
   ToolDefinition tools[];       // Dynamic array of tools belonging to this category
  };

//+------------------------------------------------------------------+
//| Theme color set structure                                        |
//+------------------------------------------------------------------+
struct ThemeColorSet
  {
   color sidebarBackground;         // Background fill color of the sidebar panel
   color sidebarBorder;             // Outline border color of the sidebar panel
   color buttonHoverBackground;     // Background fill when a category button is hovered
   color buttonActiveBackground;    // Background fill when a category button is active
   color buttonIconColor;           // Default color used to render category icons
   color buttonIconActiveColor;     // Icon color when the button is in active state
   color flyoutBackground;          // Background fill color of the flyout panel
   color flyoutBorder;              // Outline border color of the flyout panel
   color flyoutItemHoverBackground; // Background fill of a hovered flyout item row
   color flyoutTextColor;           // Default text color of flyout item labels
   color flyoutTextActiveColor;     // Text color of the active flyout item label
   color flyoutTitleColor;          // Color of the flyout panel title text
   color gripDotsColor;             // Color of the drag-grip dot indicators
   color closeButtonHoverColor;     // Background fill of the close button on hover
   color themeButtonHoverColor;     // Background fill of the theme button on hover
   color separatorColor;            // Color of the horizontal separator lines
   color accentBarColor;            // Color of the active tool accent bar indicator
   color scrollArrowColor;          // Default color of the scroll thumb pill
   color scrollArrowHoverColor;     // Color of the scroll thumb pill on hover
  };

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

Далее мы вводим перечисление "TOOL_TYPE", которое присваивает уникальный идентификатор каждому инструменту рисования в системе. Начиная с "TOOL_NONE" для отсутствия активного инструмента и "TOOL_POINTER" для курсора по умолчанию, оно перечисляет все тридцать пять инструментов вплоть до "TOOL_CHECK_MARK". Это перечисление используется всем конвейером взаимодействия и рисования для отслеживания того, какой инструмент выбрал пользователь.

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

Далее мы представляем структуру "ToolDefinition", которая объединяет тип инструмента, отображаемую метку, шрифт значка, код символа и всплывающую подсказку в единый блок. Это делает каждый инструмент самодостаточным элементом данных. Структура "CategoryDefinition" также реструктурирована: предыдущий логический флаг для нескольких инструментов заменен динамическим массивом записей "ToolDefinition". Каждая категория теперь напрямую содержит полный список инструментов, поэтому проверка наличия нескольких инструментов в категории сводится к простому считыванию размера массива.

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

Добавление растеризации треугольников к примитивам холста

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

//+------------------------------------------------------------------+
//| Fill a triangle using scanline rasterization at high resolution  |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb)
  {
   //--- Store triangle vertices as floating-point arrays for scanline processing
   double vx[3] = { (double)x0, (double)x1, (double)x2 };
   double vy[3] = { (double)y0, (double)y1, (double)y2 };
   //--- Find vertical bounding extent of the triangle
   double minY = vy[0], maxY = vy[0];
   for (int i = 1; i < 3; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; }
   //--- Iterate over each horizontal scanline within the bounding box
   for (int scanY = (int)MathCeil(minY); scanY <= (int)MathFloor(maxY); scanY++)
     {
      //--- Compute scanline center Y and prepare intersection buffer
      double cy = (double)scanY + 0.5; double xi[6]; int nc = 0;
      //--- Compute X intersections with each edge of the triangle
      for (int i = 0; i < 3; i++)
        {
         int ni = (i + 1) % 3;
         //--- Determine edge vertical extents
         double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni];
         //--- Skip edges that do not cross the current scanline
         if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue;
         //--- Compute intersection parameter along the edge
         double t = (cy - vy[i]) / (vy[ni] - vy[i]);
         if (t < 0.0 || t > 1.0) continue;
         //--- Record intersection X coordinate
         xi[nc++] = vx[i] + t * (vx[ni] - vx[i]);
        }
      //--- Sort intersections left to right
      for (int a = 0; a < nc - 1; a++)
         for (int b = a + 1; b < nc; b++)
            if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; }
      //--- Fill pixels between paired intersection spans
      for (int p = 0; p + 1 < nc; p += 2)
         for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathFloor(xi[p + 1]); fx++)
            canvas.PixelSet(fx, scanY, argb);
     }
  }

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

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

Поскольку боковая панель теперь поддерживает состояния при наведении курсора, активные выделения, выдвижные панели и индикаторы прокрутки, система тем должна охватывать все эти интерактивные элементы.

//+------------------------------------------------------------------+
//| Apply color values matching the current theme state              |
//+------------------------------------------------------------------+
void CThemeManager::ApplyTheme()
  {
   //--- Apply dark theme color assignments
   if (m_isDarkTheme)
     {
      m_themeColors.sidebarBackground         = C'30,34,45';     // Dark navy background
      m_themeColors.sidebarBorder             = C'200,210,225';  // Light blue-gray border
      m_themeColors.buttonHoverBackground     = C'30,100,200';   // Blue hover background
      m_themeColors.buttonActiveBackground    = C'41,98,255';    // Bright blue active background
      m_themeColors.buttonIconColor           = C'220,225,235';  // Near-white icon color
      m_themeColors.buttonIconActiveColor     = clrWhite;        // Pure white active icon
      m_themeColors.flyoutBackground          = C'36,41,54';     // Dark flyout background
      m_themeColors.flyoutBorder              = C'200,210,225';  // Light flyout border
      m_themeColors.flyoutItemHoverBackground = C'30,100,200';   // Blue flyout row hover
      m_themeColors.flyoutTextColor           = C'200,210,225';  // Light flyout text
      m_themeColors.flyoutTextActiveColor     = clrWhite;        // White active flyout text
      m_themeColors.flyoutTitleColor          = C'90,105,130';   // Muted blue-gray title
      m_themeColors.gripDotsColor             = C'90,100,120';   // Muted slate grip dots
      m_themeColors.closeButtonHoverColor     = C'235,55,55';    // Red close button hover
      m_themeColors.themeButtonHoverColor     = C'255,200,50';   // Yellow theme button hover
      m_themeColors.separatorColor            = C'44,50,64';     // Dark separator line
      m_themeColors.accentBarColor            = C'41,98,255';    // Bright blue accent bar
      m_themeColors.scrollArrowColor          = C'120,130,150';  // Muted scroll thumb
      m_themeColors.scrollArrowHoverColor     = clrWhite;        // White scroll thumb hover
     }
   else
     {
      //--- Apply light theme color assignments
      m_themeColors.sidebarBackground         = clrWhite;        // White background
      m_themeColors.sidebarBorder             = C'30,35,45';     // Dark border
      m_themeColors.buttonHoverBackground     = C'30,100,200';   // Blue hover background
      m_themeColors.buttonActiveBackground    = C'41,98,255';    // Bright blue active background
      m_themeColors.buttonIconColor           = C'40,45,58';     // Dark icon color
      m_themeColors.buttonIconActiveColor     = clrWhite;        // White active icon
      m_themeColors.flyoutBackground          = clrWhite;        // White flyout background
      m_themeColors.flyoutBorder              = C'30,35,45';     // Dark flyout border
      m_themeColors.flyoutItemHoverBackground = C'30,100,200';   // Blue flyout row hover
      m_themeColors.flyoutTextColor           = C'40,45,58';     // Dark flyout text
      m_themeColors.flyoutTextActiveColor     = clrWhite;        // White active flyout text
      m_themeColors.flyoutTitleColor          = C'130,140,160';  // Muted gray title
      m_themeColors.gripDotsColor             = C'160,170,185';  // Light gray grip dots
      m_themeColors.closeButtonHoverColor     = C'210,35,35';    // Red close button hover
      m_themeColors.themeButtonHoverColor     = C'150,100,0';    // Amber theme button hover
      m_themeColors.separatorColor            = C'210,215,225';  // Light separator line
      m_themeColors.accentBarColor            = C'41,98,255';    // Bright blue accent bar
      m_themeColors.scrollArrowColor          = C'120,130,145';  // Muted scroll thumb
      m_themeColors.scrollArrowHoverColor     = C'40,45,58';     // Dark scroll thumb hover
     }
  }

//+------------------------------------------------------------------+
//| Toggle between dark and light theme and reapply colors           |
//+------------------------------------------------------------------+
void CThemeManager::ToggleTheme()
  {
   //--- Flip the active theme flag and reapply color assignments
   m_isDarkTheme = !m_isDarkTheme;
   ApplyTheme();
  }

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

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

Перестройка реестра категорий в полный реестр инструментов

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

//+------------------------------------------------------------------+
//| CLASS 3 — Register all tool and category definitions             |
//+------------------------------------------------------------------+
class CToolRegistry : public CThemeManager
  {
protected:
   CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions
protected:
   //--- Populate all categories and their associated tool lists
   void          InitAllCategoriesAndTools();
   //--- Append a single tool entry to the given category tool array
   void          AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip);
   //--- Return the category that owns the given active tool type
   ENUM_CATEGORY GetCategoryForActiveTool(TOOL_TYPE activeTool);
   //--- Return the number of chart clicks required to place the given tool
   int           GetRequiredClickCount(TOOL_TYPE toolType);
   //--- Return the display label string for the given tool type
   string        GetToolLabel(TOOL_TYPE toolType);
  };

//+------------------------------------------------------------------+
//| Append a single tool entry to a category tool array             |
//+------------------------------------------------------------------+
void CToolRegistry::AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip)
  {
   //--- Expand the array by one slot to accommodate the new tool
   int sz = ArraySize(arr);
   ArrayResize(arr, sz + 1);
   //--- Populate all fields of the new tool definition
   arr[sz].toolType     = type;
   arr[sz].toolLabel    = label;
   arr[sz].iconFontName = font;
   arr[sz].iconCharCode = code;
   arr[sz].tooltipText  = tooltip;
  }

//+------------------------------------------------------------------+
//| Populate all categories and their associated tool lists          |
//+------------------------------------------------------------------+
void CToolRegistry::InitAllCategoriesAndTools()
  {
   //--- Assign Cursors category definition and reset its tool array
   m_categories[CAT_CURSORS].categoryLabel = "Cursors";
   m_categories[CAT_CURSORS].iconFontName  = ICON_CATEGORY_CURSORS.fontName;
   m_categories[CAT_CURSORS].iconCharCode  = ICON_CATEGORY_CURSORS.charCode;
   ArrayResize(m_categories[CAT_CURSORS].tools, 0);
   //--- Add pointer and crosshair tools to Cursors
   AddTool(m_categories[CAT_CURSORS].tools, TOOL_POINTER,   "Pointer",   ICON_TOOL_POINTER.fontName,   ICON_TOOL_POINTER.charCode,   "Default Pointer");
   AddTool(m_categories[CAT_CURSORS].tools, TOOL_CROSSHAIR, "Crosshair", ICON_TOOL_CROSSHAIR.fontName, ICON_TOOL_CROSSHAIR.charCode, "Crosshair / Measure");

   //--- Assign Lines category definition and reset its tool array
   m_categories[CAT_LINES].categoryLabel = "Lines";
   m_categories[CAT_LINES].iconFontName  = ICON_CATEGORY_LINES.fontName;
   m_categories[CAT_LINES].iconCharCode  = ICON_CATEGORY_LINES.charCode;
   ArrayResize(m_categories[CAT_LINES].tools, 0);
   //--- Add all line drawing tools to Lines
   AddTool(m_categories[CAT_LINES].tools, TOOL_TRENDLINE,     "Trend Line", ICON_TOOL_TRENDLINE.fontName,     ICON_TOOL_TRENDLINE.charCode,     "Trend Line");
   AddTool(m_categories[CAT_LINES].tools, TOOL_HLINE,         "Horizontal", ICON_TOOL_HLINE.fontName,         ICON_TOOL_HLINE.charCode,         "Horizontal Line");
   AddTool(m_categories[CAT_LINES].tools, TOOL_VLINE,         "Vertical",   ICON_TOOL_VLINE.fontName,         ICON_TOOL_VLINE.charCode,         "Vertical Line");
   AddTool(m_categories[CAT_LINES].tools, TOOL_RAY,           "Ray",        ICON_TOOL_RAY.fontName,           ICON_TOOL_RAY.charCode,           "Ray Line");
   AddTool(m_categories[CAT_LINES].tools, TOOL_EXTENDED_LINE, "Extended",   ICON_TOOL_EXTENDED_LINE.fontName, ICON_TOOL_EXTENDED_LINE.charCode, "Extended Line");
   AddTool(m_categories[CAT_LINES].tools, TOOL_INFO_LINE,     "Info Line",  ICON_TOOL_INFO_LINE.fontName,     ICON_TOOL_INFO_LINE.charCode,     "Info / Measure Line");

   //--- Assign Channels category definition and reset its tool array
   m_categories[CAT_CHANNELS].categoryLabel = "Channels";
   m_categories[CAT_CHANNELS].iconFontName  = ICON_CATEGORY_CHANNELS.fontName;
   m_categories[CAT_CHANNELS].iconCharCode  = ICON_CATEGORY_CHANNELS.charCode;
   ArrayResize(m_categories[CAT_CHANNELS].tools, 0);
   //--- Add all channel drawing tools to Channels
   AddTool(m_categories[CAT_CHANNELS].tools, TOOL_PARALLEL_CHANNEL,   "Parallel Channel", ICON_TOOL_PARALLEL_CH.fontName,   ICON_TOOL_PARALLEL_CH.charCode,   "Parallel Channel");
   AddTool(m_categories[CAT_CHANNELS].tools, TOOL_REGRESSION_CHANNEL, "Regression",       ICON_TOOL_REGRESSION_CH.fontName, ICON_TOOL_REGRESSION_CH.charCode, "Regression Channel");
   AddTool(m_categories[CAT_CHANNELS].tools, TOOL_STDDEV_CHANNEL,     "Std Deviation",    ICON_TOOL_STDDEV_CH.fontName,     ICON_TOOL_STDDEV_CH.charCode,     "Standard Deviation Channel");

   //--- Assign Pitchfork category definition and reset its tool array
   m_categories[CAT_PITCHFORK].categoryLabel = "Pitchfork";
   m_categories[CAT_PITCHFORK].iconFontName  = ICON_CATEGORY_PITCHFORK.fontName;
   m_categories[CAT_PITCHFORK].iconCharCode  = ICON_CATEGORY_PITCHFORK.charCode;
   ArrayResize(m_categories[CAT_PITCHFORK].tools, 0);
   //--- Add all pitchfork drawing tools to Pitchfork
   AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_PITCHFORK,        "Andrew's Fork", ICON_TOOL_PITCHFORK.fontName,  ICON_TOOL_PITCHFORK.charCode,  "Andrew's Pitchfork");
   AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_SCHIFF_PITCHFORK, "Schiff Fork",   ICON_TOOL_SCHIFF.fontName,     ICON_TOOL_SCHIFF.charCode,     "Schiff Pitchfork");
   AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_MOD_SCHIFF,       "Mod. Schiff",   ICON_TOOL_MOD_SCHIFF.fontName, ICON_TOOL_MOD_SCHIFF.charCode, "Modified Schiff Pitchfork");

   //--- Assign Gann category definition and reset its tool array
   m_categories[CAT_GANN].categoryLabel = "Gann";
   m_categories[CAT_GANN].iconFontName  = ICON_CATEGORY_GANN.fontName;
   m_categories[CAT_GANN].iconCharCode  = ICON_CATEGORY_GANN.charCode;
   ArrayResize(m_categories[CAT_GANN].tools, 0);
   //--- Add all Gann drawing tools to Gann
   AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_LINE, "Gann Line", ICON_TOOL_GANN_LINE.fontName, ICON_TOOL_GANN_LINE.charCode, "Gann Line");
   AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_FAN,  "Gann Fan",  ICON_TOOL_GANN_FAN.fontName,  ICON_TOOL_GANN_FAN.charCode,  "Gann Fan");
   AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_GRID, "Gann Grid", ICON_TOOL_GANN_GRID.fontName, ICON_TOOL_GANN_GRID.charCode, "Gann Grid");

   //--- Assign Fibonacci category definition and reset its tool array
   m_categories[CAT_FIBONACCI].categoryLabel = "Fibonacci";
   m_categories[CAT_FIBONACCI].iconFontName  = ICON_CATEGORY_FIBONACCI.fontName;
   m_categories[CAT_FIBONACCI].iconCharCode  = ICON_CATEGORY_FIBONACCI.charCode;
   ArrayResize(m_categories[CAT_FIBONACCI].tools, 0);
   //--- Add all Fibonacci drawing tools to Fibonacci
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_RETRACEMENT, "Retracement", ICON_TOOL_FIBO_RET.fontName,  ICON_TOOL_FIBO_RET.charCode,  "Fibonacci Retracement");
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_EXPANSION,   "Expansion",   ICON_TOOL_FIBO_EXP.fontName,  ICON_TOOL_FIBO_EXP.charCode,  "Fibonacci Expansion");
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_CHANNEL,     "Fib Channel", ICON_TOOL_FIBO_CH.fontName,   ICON_TOOL_FIBO_CH.charCode,   "Fibonacci Channel");
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_TIMEZONES,   "Time Zones",  ICON_TOOL_FIBO_TZ.fontName,   ICON_TOOL_FIBO_TZ.charCode,   "Fibonacci Time Zones");
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_FAN,         "Fib Fan",     ICON_TOOL_FIBO_FAN.fontName,  ICON_TOOL_FIBO_FAN.charCode,  "Fibonacci Fan");
   AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_ARCS,        "Fib Arcs",    ICON_TOOL_FIBO_ARCS.fontName, ICON_TOOL_FIBO_ARCS.charCode, "Fibonacci Arcs");

   //--- Assign Shapes category definition and reset its tool array
   m_categories[CAT_SHAPES].categoryLabel = "Shapes";
   m_categories[CAT_SHAPES].iconFontName  = ICON_CATEGORY_SHAPES.fontName;
   m_categories[CAT_SHAPES].iconCharCode  = ICON_CATEGORY_SHAPES.charCode;
   ArrayResize(m_categories[CAT_SHAPES].tools, 0);
   //--- Add all shape drawing tools to Shapes
   AddTool(m_categories[CAT_SHAPES].tools, TOOL_RECTANGLE, "Rectangle", ICON_TOOL_RECTANGLE.fontName, ICON_TOOL_RECTANGLE.charCode, "Rectangle");
   AddTool(m_categories[CAT_SHAPES].tools, TOOL_TRIANGLE,  "Triangle",  ICON_TOOL_TRIANGLE.fontName,  ICON_TOOL_TRIANGLE.charCode,  "Triangle");
   AddTool(m_categories[CAT_SHAPES].tools, TOOL_ELLIPSE,   "Ellipse",   ICON_TOOL_ELLIPSE.fontName,   ICON_TOOL_ELLIPSE.charCode,   "Ellipse");

   //--- Assign Annotations category definition and reset its tool array
   m_categories[CAT_ANNOTATIONS].categoryLabel = "Annotate";
   m_categories[CAT_ANNOTATIONS].iconFontName  = ICON_CATEGORY_ANNOTATIONS.fontName;
   m_categories[CAT_ANNOTATIONS].iconCharCode  = ICON_CATEGORY_ANNOTATIONS.charCode;
   ArrayResize(m_categories[CAT_ANNOTATIONS].tools, 0);
   //--- Add all annotation tools to Annotations
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_TEXT,        "Text",        ICON_TOOL_TEXT.fontName,        ICON_TOOL_TEXT.charCode,        "Text Label");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_UP,    "Arrow Up",    ICON_TOOL_ARROW_UP.fontName,    ICON_TOOL_ARROW_UP.charCode,    "Arrow Up");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_DOWN,  "Arrow Down",  ICON_TOOL_ARROW_DOWN.fontName,  ICON_TOOL_ARROW_DOWN.charCode,  "Arrow Down");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_UP,    "Thumb Up",    ICON_TOOL_THUMB_UP.fontName,    ICON_TOOL_THUMB_UP.charCode,    "Thumbs Up");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_DOWN,  "Thumb Down",  ICON_TOOL_THUMB_DOWN.fontName,  ICON_TOOL_THUMB_DOWN.charCode,  "Thumbs Down");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_PRICE_LABEL, "Price Label", ICON_TOOL_PRICE_LABEL.fontName, ICON_TOOL_PRICE_LABEL.charCode, "Left Price Label");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_STOP_SIGN,   "Stop Sign",   ICON_TOOL_STOP_SIGN.fontName,   ICON_TOOL_STOP_SIGN.charCode,   "Stop Sign");
   AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_CHECK_MARK,  "Check Mark",  ICON_TOOL_CHECK_MARK.fontName,  ICON_TOOL_CHECK_MARK.charCode,  "Check Mark");
  }

//+------------------------------------------------------------------+
//| Return the category that owns the given active tool type         |
//+------------------------------------------------------------------+
ENUM_CATEGORY CToolRegistry::GetCategoryForActiveTool(TOOL_TYPE activeTool)
  {
   //--- Return no category for inactive or pointer tool states
   if (activeTool == TOOL_NONE || activeTool == TOOL_POINTER) return CAT_NONE;
   //--- Search all categories and their tool lists for a match
   for (int c = 0; c < CAT_COUNT; c++)
      for (int t = 0; t < ArraySize(m_categories[c].tools); t++)
         if (m_categories[c].tools[t].toolType == activeTool) return (ENUM_CATEGORY)c;
   return CAT_NONE;
  }

//+------------------------------------------------------------------+
//| Return click count required to place the given tool              |
//+------------------------------------------------------------------+
int CToolRegistry::GetRequiredClickCount(TOOL_TYPE toolType)
  {
   switch (toolType)
     {
      //--- Cursor tools require no chart clicks
      case TOOL_POINTER: case TOOL_CROSSHAIR:
         return 0;
      //--- Single-click tools are placed with one chart interaction
      case TOOL_HLINE: case TOOL_VLINE: case TOOL_TEXT: case TOOL_ARROW_UP: case TOOL_ARROW_DOWN:
      case TOOL_THUMB_UP: case TOOL_THUMB_DOWN: case TOOL_PRICE_LABEL: case TOOL_STOP_SIGN:
      case TOOL_CHECK_MARK: case TOOL_FIBO_TIMEZONES:
         return 1;
      //--- Two-click tools require a start and end point
      case TOOL_TRENDLINE: case TOOL_RAY: case TOOL_EXTENDED_LINE: case TOOL_INFO_LINE:
      case TOOL_RECTANGLE: case TOOL_TRIANGLE: case TOOL_ELLIPSE: case TOOL_FIBO_RETRACEMENT:
      case TOOL_FIBO_EXPANSION: case TOOL_FIBO_FAN: case TOOL_FIBO_ARCS: case TOOL_GANN_LINE:
      case TOOL_GANN_FAN: case TOOL_GANN_GRID: case TOOL_REGRESSION_CHANNEL: case TOOL_STDDEV_CHANNEL:
         return 2;
      //--- Three-click tools require three anchor points
      case TOOL_PARALLEL_CHANNEL: case TOOL_FIBO_CHANNEL: case TOOL_PITCHFORK:
      case TOOL_SCHIFF_PITCHFORK: case TOOL_MOD_SCHIFF:
         return 3;
      //--- Default to single click for unrecognized tool types
      default: return 1;
     }
  }

//+------------------------------------------------------------------+
//| Return the display label string for the given tool type          |
//+------------------------------------------------------------------+
string CToolRegistry::GetToolLabel(TOOL_TYPE toolType)
  {
   //--- Search all categories and tool lists for a label match
   for (int c = 0; c < CAT_COUNT; c++)
      for (int t = 0; t < ArraySize(m_categories[c].tools); t++)
         if (m_categories[c].tools[t].toolType == toolType) return m_categories[c].tools[t].toolLabel;
   return "None";
  }

В этой части мы объявляем класс "CToolRegistry", который заменяет предыдущий "CCategoryRegistry" и наследует от "CThemeManager". Он содержит тот же массив категорий, но теперь объявляет пять защищенных методов: "InitAllCategoriesAndTools" для заполнения всех категорий и их инструментов, "AddTool" в качестве вспомогательного метода для добавления записей инструментов, "GetCategoryForActiveTool" для обратного поиска категории, которой принадлежит данный инструмент, "GetRequiredClickCount" для определения количества кликов по графику, необходимых инструменту, и "GetToolLabel" для получения отображаемого имени инструмента.

Метод "AddTool" расширяет заданный массив инструментов на один слот с помощью функции ArrayResize и заполняет новую запись типом инструмента, меткой, шрифтом значка, кодом символа и всплывающей подсказкой. Это обеспечивает чистоту и согласованность заполнения категорий.

Мы реализуем метод "InitAllCategoriesAndTools", присваивая каждой категории ее метку и значок из глобальных определений значков, обнуляя массив инструментов, а затем многократно вызывая метод "AddTool" для регистрации каждого инструмента. Курсоры получают указатель и перекрестие. Линии получают шесть инструментов от линии тренда до информационной линии. Категории каналов, вил и инструментов Ганна получают по три инструмента. Фибоначчи получает самый большой набор из шести инструментов, охватывающих коррекцию, расширение, канал, временные зоны, веер и дуги. Фигуры содержат прямоугольники, треугольники и эллипсы. Аннотации содержат восемь инструментов от текстовых меток до галочек.

Метод "GetCategoryForActiveTool" возвращает информацию о том, какой категории принадлежит данный активный инструмент, путем поиска по всем категориям и их массивам инструментов, возвращая "CAT_NONE" для неактивных состояний или состояний указателя. Метод "GetRequiredClickCount" использует оператор switch для классификации каждого инструмента по количеству кликов: ноль кликов для курсоров, один клик для горизонтальных линий, вертикальных линий, аннотаций и аналогичных инструментов с одной точкой, два клика для линий тренда, прямоугольников, коррекций Фибоначчи и других инструментов с двумя якорями, а три клика для параллельных каналов, каналов Фибоначчи и вариантов вил. Метод "GetToolLabel" ищет во всех категориях соответствующий тип инструмента и возвращает его отображаемую метку. Теперь мы расширим класс canvases, чтобы расширить слои с выпадающими поверхностями.

Расширение слоя холст с помощью поверхностей выдвижных панелей

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

//+------------------------------------------------------------------+
//| CLASS 4 — Create, destroy, and resize all canvas layers          |
//+------------------------------------------------------------------+
class CCanvasLayer : public CToolRegistry
  {
protected:
   int     m_supersampleFactor;       // Supersampling multiplier for high-res rendering
   long    m_chartId;                 // Chart identifier this layer belongs to
   CCanvas m_canvasSidebar;           // Final display-resolution sidebar canvas
   CCanvas m_canvasSidebarHighRes;    // High-resolution sidebar canvas for supersampling
   CCanvas m_canvasFlyout;            // Final display-resolution flyout canvas
   CCanvas m_canvasFlyoutHighRes;     // High-resolution flyout canvas for supersampling
   string  m_nameSidebar;             // Object name of the sidebar bitmap label
   string  m_nameFlyout;              // Object name of the flyout bitmap label

protected:
   //--- Create all canvas objects at the given dimensions
   bool CreateAllCanvases(int w, int h);
   //--- Destroy all canvas objects and remove chart objects
   void DestroyAllCanvases();
   //--- Resize both sidebar canvases to the given dimensions
   void ResizeSidebarCanvases(int w, int h);
  };

//+------------------------------------------------------------------+
//| Create all canvas objects at the given dimensions                |
//+------------------------------------------------------------------+
bool CCanvasLayer::CreateAllCanvases(int w, int h)
  {
   //--- Create the display-resolution sidebar bitmap label canvas
   if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create sidebar canvas"); return false; }
   //--- Create the high-resolution sidebar canvas for supersampled drawing
   if (!m_canvasSidebarHighRes.Create("ToolsPalette_SidebarHR", w * m_supersampleFactor, h * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create sidebar HR canvas"); return false; }
   //--- Create the display-resolution flyout bitmap label canvas
   if (!m_canvasFlyout.CreateBitmapLabel(0, 0, m_nameFlyout, 0, 0, 200, 200, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create flyout canvas"); return false; }
   //--- Create the high-resolution flyout canvas for supersampled drawing
   if (!m_canvasFlyoutHighRes.Create("ToolsPalette_FlyoutHR", 200 * m_supersampleFactor, 200 * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create flyout HR canvas"); return false; }
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and remove chart objects              |
//+------------------------------------------------------------------+
void CCanvasLayer::DestroyAllCanvases()
  {
   //--- Destroy display canvas and remove its chart object
   m_canvasSidebar.Destroy();
   ObjectDelete(0, m_nameSidebar);
   //--- Destroy the high-resolution sidebar working canvas
   m_canvasSidebarHighRes.Destroy();
   //--- Destroy flyout canvas and remove its chart object
   m_canvasFlyout.Destroy();
   ObjectDelete(0, m_nameFlyout);
   //--- Destroy the high-resolution flyout working canvas
   m_canvasFlyoutHighRes.Destroy();
  }

Мы объявляем класс "CCanvasLayer", который теперь наследует от "CToolRegistry" и добавляет два новых члена canvas к существующей паре боковой панели: canvas для выдвижной панели с разрешением экрана и его аналог с высоким разрешением для рендеринга с суперсэмплированием, а также строку, содержащую имя bitmap label-объекта всплывающего окна.

Метод "CreateAllCanvases" теперь создает четыре холста последовательно. После создания дисплея боковой панели и холстов высокого разрешения, как и прежде, мы создаем холст для выдвижной панели в виде bitmap label-объекта с помощью функции CreateBitmapLabel с начальным размером двести на двести пикселей, а затем его рабочий холст высокого разрешения, масштабированный с помощью коэффициента суперсэмплирования. Если какой-либо из четырех способов создания не удается, мы выводим ошибку и возвращаем false.

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

Расширение компоновки боковой панели с прокруткой, перетаскиванием, изменением размера и проверкой попадания

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

//+------------------------------------------------------------------+
//| CLASS 5 — Compute and maintain sidebar layout and geometry       |
//+------------------------------------------------------------------+
class CSidebarLayout : public CCanvasLayer
  {
protected:
   int             m_panelX;                    // Horizontal position of the sidebar panel
   int             m_panelY;                    // Vertical position of the sidebar panel
   int             m_sidebarWidth;              // Width of the sidebar panel in pixels
   int             m_sidebarHeight;             // Height of the sidebar panel in pixels
   int             m_categoryButtonSize;        // Size of each category button in pixels
   int             m_categoryButtonPadding;     // Vertical gap between category buttons
   int             m_panelCornerRadius;         // Corner rounding radius of the panel
   int             m_headerGripHeight;          // Height of the top header and grip area
   ENUM_SNAP_STATE m_snapState;                 // Current snap alignment state
   int             m_sidebarMaxVisibleCats;     // Maximum number of visible category buttons
   int             m_sidebarScrollPixels;       // Current vertical scroll offset in pixels
   int             m_sidebarScrollThumbHeight;  // Height of the sidebar scroll thumb pill
   int             m_sidebarScrollThinWidth;    // Width of the sidebar scroll thumb pill
   bool            m_isSidebarThumbDragging;    // Flag indicating scroll thumb drag in progress
   int             m_sidebarThumbDragStartY;    // Mouse Y when sidebar thumb drag started
   int             m_sidebarThumbDragStartPixels;// Scroll offset when sidebar thumb drag started
   bool            m_isHoveredSidebarScrollArea;// Flag indicating mouse is over sidebar scroll area
   bool            m_isHoveredSidebarThumb;     // Flag indicating mouse is over sidebar scroll thumb
   bool            m_isPanelDragging;           // Flag indicating panel drag in progress
   int             m_dragOffsetX;               // Mouse X offset from panel origin when drag started
   int             m_dragOffsetY;               // Mouse Y offset from panel origin when drag started
   bool            m_isResizingBottomEdge;      // Flag indicating bottom resize drag in progress
   int             m_bottomResizeDragStartY;    // Mouse Y when bottom resize drag started
   int             m_bottomResizeStartHeight;   // Panel height when bottom resize drag started
   int             m_snappedSidebarHeight;      // User-set height override while panel is snapped
   bool            m_isBottomResizeHovered;     // Flag indicating mouse is over the bottom resize grip

protected:
   //--- Compute and set the sidebar panel height based on available chart space
   void          CalcSidebarHeight();
   //--- Compute the Y pixel position of a category button by index
   int           CalcCategoryButtonY(int idx);
   //--- Compute the top clipping boundary for the category button area
   int           CalcClipTop();
   //--- Compute the bottom clipping boundary for the category button area
   int           CalcClipBottom();
   //--- Compute total pixel height of all category buttons stacked
   int           CalcSidebarTotalScrollPixels();
   //--- Compute the visible viewport pixel height for category buttons
   int           CalcSidebarViewportPixels();
   //--- Compute the maximum allowable scroll offset in pixels
   int           CalcSidebarMaxScrollPixels();
   //--- Check whether the category button at the given index is within the visible clip area
   bool          IsCategoryButtonVisible(int idx);
   //--- Attempt to snap the panel to a chart edge based on current position
   void          TrySnapToEdge();
   //--- Test whether the given screen coordinates hit the sidebar panel
   bool          HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly);
   //--- Return the category under the given local coordinates, or CAT_NONE
   ENUM_CATEGORY HitTestCategoryButton(int lx, int ly);
   //--- Test whether the given local coordinates hit the grip drag area
   bool          HitTestOverGripArea(int lx, int ly);
   //--- Test whether the given local coordinates hit the close button area
   bool          HitTestOverCloseButton(int lx, int ly);
   //--- Test whether the given local coordinates hit the theme toggle button area
   bool          HitTestOverThemeButton(int lx, int ly);
   //--- Test whether the given local coordinates hit the bottom resize grip
   bool          HitTestOverBottomResizeGrip(int lx, int ly);
  };

//+------------------------------------------------------------------+
//| Compute and set sidebar height based on available chart space    |
//+------------------------------------------------------------------+
void CSidebarLayout::CalcSidebarHeight()
  {
   //--- Get current chart height in pixels
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //--- Define vertical padding constants
   int topPad = 8, botPad = 10;
   //--- Set button gap spacing
   m_categoryButtonPadding = 6;
   //--- Handle snapped panel height computation
   if (m_snapState != SNAP_FLOAT)
     {
      //--- Pin panel to fixed Y offset below chart top
      m_panelY = 30;
      ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
      //--- Compute maximum available height below panel top offset
      int availH   = chartH - m_panelY - 8;
      //--- Compute ideal natural height to fit all category buttons
      int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
      //--- Compute minimum height to show at least three category buttons
      int minH     = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
      //--- Apply user-set snapped height override if present, otherwise use natural size
      if (m_snappedSidebarHeight > 0)
         m_sidebarHeight = MathMax(minH, MathMin(MathMin(naturalH, availH), m_snappedSidebarHeight));
      else
         m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH));
     }
   else
     {
      //--- Compute natural and maximum height bounds for a floating panel
      int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
      int maxH     = chartH - m_panelY - 20;
      int minH     = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad;
      //--- Clamp the floating panel height within valid bounds
      if (m_sidebarHeight < minH || m_sidebarHeight > MathMin(naturalH, maxH))
         m_sidebarHeight = MathMin(naturalH, maxH);
     }
   //--- Compute usable height for the button area
   int btnAreaH = m_sidebarHeight - m_headerGripHeight - topPad - botPad;
   //--- Compute total height needed for all buttons at natural spacing
   int fullBtnH = CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding;
   //--- All buttons fit: show all and clear scroll offset
   if (fullBtnH <= btnAreaH)
     {
      m_sidebarMaxVisibleCats = CAT_COUNT;
      m_sidebarScrollPixels   = 0;
     }
   else
     {
      //--- Compute how many buttons fit and clamp scroll offset within valid range
      m_sidebarMaxVisibleCats = MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding)));
      m_sidebarScrollPixels   = MathMax(0, MathMin(m_sidebarScrollPixels, CalcSidebarMaxScrollPixels()));
     }
  }

//+------------------------------------------------------------------+
//| Compute Y pixel position of a category button by index           |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcCategoryButtonY(int idx)
  {
   //--- Return scroll-adjusted Y offset below the header grip area
   return m_headerGripHeight + 8 + idx * (m_categoryButtonSize + m_categoryButtonPadding) - m_sidebarScrollPixels;
  }

//+------------------------------------------------------------------+
//| Compute top clip boundary for the category button area           |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcClipTop()
  {
   //--- Return Y position just below the header grip bottom edge
   return m_headerGripHeight + 8;
  }

//+------------------------------------------------------------------+
//| Compute bottom clip boundary for the category button area        |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcClipBottom()
  {
   //--- Return Y position leaving bottom padding inside the panel
   return m_sidebarHeight - 10;
  }

//+------------------------------------------------------------------+
//| Compute total pixel height of all category buttons stacked       |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcSidebarTotalScrollPixels()
  {
   //--- Return the combined height of all buttons including inter-button gaps
   return CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding;
  }

//+------------------------------------------------------------------+
//| Compute the visible viewport pixel height for category buttons   |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcSidebarViewportPixels()
  {
   //--- Return the pixel height of the visible button clip region
   return CalcClipBottom() - CalcClipTop();
  }

//+------------------------------------------------------------------+
//| Compute the maximum allowable scroll offset in pixels            |
//+------------------------------------------------------------------+
int CSidebarLayout::CalcSidebarMaxScrollPixels()
  {
   //--- Return zero if all buttons fit; otherwise return the overflow amount
   return MathMax(0, CalcSidebarTotalScrollPixels() - CalcSidebarViewportPixels());
  }

//+------------------------------------------------------------------+
//| Check whether a category button is within the visible clip area  |
//+------------------------------------------------------------------+
bool CSidebarLayout::IsCategoryButtonVisible(int idx)
  {
   //--- All buttons are visible when scroll is not needed
   if (m_sidebarMaxVisibleCats >= CAT_COUNT) return true;
   //--- Compute the button's scroll-adjusted Y position
   int y = CalcCategoryButtonY(idx);
   //--- Return true if the button overlaps the clip region
   return (y + m_categoryButtonSize > CalcClipTop() && y < CalcClipBottom());
  }

//+------------------------------------------------------------------+
//| Attempt to snap the panel to a chart edge based on position      |
//+------------------------------------------------------------------+
void CSidebarLayout::TrySnapToEdge()
  {
   //--- Get current chart width for right-edge detection
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   ENUM_SNAP_STATE prev = m_snapState;
   //--- Snap to left edge if panel is within the snap threshold
   if (m_panelX <= SnapThreshold)
     {
      m_snapState = SNAP_LEFT;
      m_panelX    = 0;
      //--- Clear snapped height override when transitioning from float
      if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0;
     }
   //--- Snap to right edge if panel right boundary is within the snap threshold
   else if (m_panelX + m_sidebarWidth >= chartW - SnapThreshold)
     {
      m_snapState = SNAP_RIGHT;
      m_panelX    = chartW - m_sidebarWidth;
      if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0;
     }
   else
     {
      //--- Set floating state and clear snapped height when leaving a snapped edge
      m_snapState = SNAP_FLOAT;
      if (prev != SNAP_FLOAT) { m_snappedSidebarHeight = 0; m_categoryButtonPadding = 6; }
     }
   //--- Update the chart object X position to reflect the snapped or free position
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
  }

//+------------------------------------------------------------------+
//| Test whether screen coordinates hit the sidebar panel            |
//+------------------------------------------------------------------+
bool CSidebarLayout::HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly)
  {
   //--- Compute local coordinates relative to the panel origin
   lx = mouseX - m_panelX;
   ly = mouseY - m_panelY;
   //--- Return true if local coordinates fall within the panel bounds
   return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_sidebarHeight);
  }

//+------------------------------------------------------------------+
//| Return the category under local coordinates, or CAT_NONE         |
//+------------------------------------------------------------------+
ENUM_CATEGORY CSidebarLayout::HitTestCategoryButton(int lx, int ly)
  {
   //--- Reject coordinates outside the category button clip region
   if (ly < CalcClipTop() || ly >= CalcClipBottom()) return CAT_NONE;
   //--- Compute horizontal start of the centered button column
   int btnX = (m_sidebarWidth - m_categoryButtonSize) / 2;
   //--- Test each visible category button for a hit
   for (int c = 0; c < CAT_COUNT; c++)
     {
      if (!IsCategoryButtonVisible(c)) continue;
      int btnY = CalcCategoryButtonY(c);
      //--- Return category if local coordinates fall within the button bounds
      if (lx >= btnX && lx <= btnX + m_categoryButtonSize &&
          ly >= btnY && ly <= btnY + m_categoryButtonSize && ly < m_sidebarHeight - 8)
         return (ENUM_CATEGORY)c;
     }
   return CAT_NONE;
  }

//+------------------------------------------------------------------+
//| Test whether local coordinates hit the grip drag area            |
//+------------------------------------------------------------------+
bool CSidebarLayout::HitTestOverGripArea(int lx, int ly)
  {
   //--- Return true if coordinates fall within the horizontal grip strip
   return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize && ly < m_categoryButtonSize + 20);
  }

//+------------------------------------------------------------------+
//| Test whether local coordinates hit the close button area         |
//+------------------------------------------------------------------+
bool CSidebarLayout::HitTestOverCloseButton(int lx, int ly)
  {
   //--- Return true if coordinates fall within the top close button slot
   return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_categoryButtonSize);
  }

//+------------------------------------------------------------------+
//| Test whether local coordinates hit the theme toggle button area  |
//+------------------------------------------------------------------+
bool CSidebarLayout::HitTestOverThemeButton(int lx, int ly)
  {
   //--- Return true if coordinates fall within the theme toggle row
   return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize + 20 && ly < m_headerGripHeight);
  }

//+------------------------------------------------------------------+
//| Test whether local coordinates hit the bottom resize grip        |
//+------------------------------------------------------------------+
bool CSidebarLayout::HitTestOverBottomResizeGrip(int lx, int ly)
  {
   //--- Return true if coordinates fall within the bottom resize handle strip
   return (lx >= 0 && lx < m_sidebarWidth && ly >= m_sidebarHeight - 8 && ly < m_sidebarHeight);
  }

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

Список методов также расширяется. В дополнение к существующим методам расчета высоты, позиционирования кнопок и определения границ фрагмента, мы добавляем функции "CalcSidebarTotalScrollPixels", которая возвращает суммарную высоту всех кнопок категорий, расположенных друг над другом, "CalcSidebarViewportPixels", которая возвращает высоту видимой области фрагмента, "CalcSidebarMaxScrollPixels", которая вычисляет максимальное смещение прокрутки, а также "IsCategoryButtonVisible", которая проверяет, попадает ли кнопка в видимую область после прокрутки. Мы также добавляем "TrySnapToEdge" для привязки панели к краям графика после перетаскивания, а также шесть методов проверки попадания: "HitTestOverSidebar" для проверки нахождения курсора мыши над панелью, "HitTestCategoryButton" для определения того, какая кнопка категории находится под курсором, и отдельные проверки для области захвата, кнопки закрытия, кнопки темы и нижней области изменения размера.

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

Внедрение класса выдвижной панели

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

//+------------------------------------------------------------------+
//| CLASS 6 — Manage the flyout tool selection panel                 |
//+------------------------------------------------------------------+
class CFlyoutPanel : public CSidebarLayout
  {
protected:
   int           m_flyoutWidth;               // Width of the flyout body (excluding pointer triangle)
   int           m_flyoutItemHeight;          // Height of each tool item row in the flyout
   int           m_flyoutPadding;             // Horizontal and vertical padding inside the flyout
   int           m_flyoutPointerWidth;        // Half-height of the pointer triangle
   int           m_flyoutPointerHeight;       // Depth (horizontal extent) of the pointer triangle
   int           m_flyoutPointerLocalY;       // Local Y center of the pointer tip within the flyout
   bool          m_flyoutPointerOnLeft;       // Flag indicating the pointer faces left toward the sidebar
   bool          m_isFlyoutVisible;           // Flag indicating the flyout is currently visible
   ENUM_CATEGORY m_flyoutActiveCat;           // Category whose tools are currently shown in the flyout
   int           m_hoveredFlyoutItem;         // Index of the hovered flyout item row, or -1
   int           m_flyoutScrollPixels;        // Current vertical scroll offset of the flyout list
   int           m_flyoutMaxVisibleItems;     // Maximum number of visible tool rows in the flyout
   int           m_flyoutScrollThumbHeight;   // Height of the flyout scroll thumb pill
   bool          m_isFlyoutThumbDragging;     // Flag indicating flyout scroll thumb drag in progress
   int           m_flyoutThumbDragStartY;     // Mouse Y when flyout thumb drag started
   int           m_flyoutThumbDragStartPixels;// Scroll offset when flyout thumb drag started
   bool          m_isHoveredFlyoutScrollArea; // Flag indicating mouse is over the flyout scroll area
   bool          m_isHoveredFlyoutThumb;      // Flag indicating mouse is over the flyout scroll thumb

protected:
   //--- Show the flyout panel for the given category, highlighting the active tool
   void ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool);
   //--- Hide the flyout panel and reset its state
   void HideFlyout();
   //--- Draw and composite the full flyout panel for the given category
   void DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool);
   //--- Draw the flyout scroll thumb pill overlay onto the display canvas
   void DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat);
   //--- Draw the flyout body border at high resolution
   void DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor);
   //--- Test whether screen coordinates hit the visible flyout panel
   bool HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly);
   //--- Return the flyout item index under the given local coordinates, or -1
   int  HitTestFlyoutItem(int lx, int ly);
  };

//+------------------------------------------------------------------+
//| Hide the flyout panel and reset its state                        |
//+------------------------------------------------------------------+
void CFlyoutPanel::HideFlyout()
  {
   //--- Reset hover item and scroll offset
   m_hoveredFlyoutItem = -1;
   m_flyoutScrollPixels = 0;
   //--- Clear scroll hover flags
   m_isHoveredFlyoutScrollArea = false;
   m_isHoveredFlyoutThumb      = false;
   //--- Hide the flyout chart object from all timeframes
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   //--- Mark the flyout as hidden and clear the active category
   m_isFlyoutVisible = false;
   m_flyoutActiveCat = CAT_NONE;
  }

//+------------------------------------------------------------------+
//| Test whether screen coordinates hit the visible flyout panel     |
//+------------------------------------------------------------------+
bool CFlyoutPanel::HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly)
  {
   //--- Skip test if flyout is not visible
   if (!m_isFlyoutVisible) return false;
   //--- Read the flyout chart object position and size
   int fx = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE);
   int fy = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE);
   int fw = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XSIZE);
   int fh = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YSIZE);
   //--- Compute local coordinates relative to the flyout origin
   lx = mouseX - fx;
   ly = mouseY - fy;
   //--- Return true if the mouse is within the flyout bounds
   return (mouseX >= fx && mouseX < fx + fw && mouseY >= fy && mouseY < fy + fh);
  }

//+------------------------------------------------------------------+
//| Return flyout item index under local coordinates, or -1          |
//+------------------------------------------------------------------+
int CFlyoutPanel::HitTestFlyoutItem(int lx, int ly)
  {
   //--- Return no hit if no category is active
   if (m_flyoutActiveCat == CAT_NONE) return -1;
   int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
   //--- Compute title row height and body left offset for pointer direction
   int titleH = 26, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0;
   int visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems);
   //--- Exclude clicks on the scroll thumb column when scrolling is active
   if (nTools > m_flyoutMaxVisibleItems)
     {
      int tw = m_sidebarScrollThinWidth;
      if (!m_flyoutPointerOnLeft) { if (lx <= dispBx + tw + 8) return -1; }
      else                        { if (lx >= dispBx + m_flyoutWidth - tw - 8) return -1; }
     }
   //--- Compute the vertical clip region for item rows
   int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight;
   //--- Return no hit if Y is outside the item clip region
   if (ly < itemClipTop || ly >= itemClipBot) return -1;
   //--- Compute item index from Y position accounting for scroll offset
   int idx = (ly - itemClipTop + m_flyoutScrollPixels) / m_flyoutItemHeight;
   if (idx < 0 || idx >= nTools) return -1;
   return idx;
  }

//+------------------------------------------------------------------+
//| Show the flyout for the given category with active tool state    |
//+------------------------------------------------------------------+
void CFlyoutPanel::ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool)
  {
   //--- Hide flyout and exit if the category has no tools
   int nTools = ArraySize(m_categories[(int)cat].tools);
   if (nTools == 0) { HideFlyout(); return; }
   //--- Reset the flyout scroll offset on each new show
   m_flyoutScrollPixels = 0;
   //--- Compute flyout panel height based on visible tool count
   int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems);
   int flyH   = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding;
   int totalW = m_flyoutWidth + m_flyoutPointerHeight;
   //--- Read chart dimensions for bounds checking
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //--- Determine pointer direction and flyout X position based on snap state
   bool ptrLeft; int flyX;
   if (m_snapState == SNAP_LEFT)
     {
      ptrLeft = true;
      flyX    = m_panelX + m_sidebarWidth;
     }
   else if (m_snapState == SNAP_RIGHT)
     {
      ptrLeft = false;
      flyX    = m_panelX - totalW;
     }
   else
     {
      //--- For floating panels, prefer opening to the right with fallback to left
      int rightX = m_panelX + m_sidebarWidth;
      if (rightX + totalW <= chartW - 4)
        { ptrLeft = true; flyX = rightX; }
      else
        {
         ptrLeft = false; flyX = m_panelX - totalW;
         if (flyX < 0) { ptrLeft = true; flyX = rightX; }
        }
     }
   m_flyoutPointerOnLeft = ptrLeft;
   //--- Compute the flyout Y position aligned to the hovered category button center
   int btnCentreY = m_panelY + CalcCategoryButtonY((int)cat) + m_categoryButtonSize / 2;
   int flyY = btnCentreY - (titleH + m_flyoutPadding + 6);
   //--- Clamp flyout Y within chart bounds
   if (flyY + flyH > chartH - 8) flyY = chartH - flyH - 8;
   if (flyY < 4) flyY = 4;
   //--- Clamp the pointer local Y to stay within the flyout rounded corners
   m_flyoutPointerLocalY = MathMax(m_panelCornerRadius + m_flyoutPointerWidth + 2,
                           MathMin(flyH - m_panelCornerRadius - m_flyoutPointerWidth - 2, btnCentreY - flyY));
   //--- Position the flyout chart object and mark it visible
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE, flyX);
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE, flyY);
   m_isFlyoutVisible = true;
   m_flyoutActiveCat = cat;
   //--- Draw the flyout contents and make the object visible on all timeframes
   DrawFlyoutForCategory(cat, activeTool);
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
  }

//+------------------------------------------------------------------+
//| Draw the flyout body border at high resolution                   |
//+------------------------------------------------------------------+
void CFlyoutPanel::DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor)
  {
   //--- Skip drawing when border width is disabled
   if (BorderWidth <= 0) return;
   //--- Clamp corner radius and compute half-thickness offset
   r = MathMin(r, MathMin(w / 2, h / 2)); int h2 = thickness / 2;
   //--- Draw all four border edges of the flyout body
   DrawBorderEdge(m_canvasFlyoutHighRes, x + r,      y + h2,     x + w - r,  y + h2,     thickness, borderColor);
   DrawBorderEdge(m_canvasFlyoutHighRes, x + w - h2, y + r,      x + w - h2, y + h - r,  thickness, borderColor);
   DrawBorderEdge(m_canvasFlyoutHighRes, x + w - r,  y + h - h2, x + r,      y + h - h2, thickness, borderColor);
   DrawBorderEdge(m_canvasFlyoutHighRes, x + h2,     y + h - r,  x + h2,     y + r,      thickness, borderColor);
   //--- Draw all four corner arcs of the flyout body
   DrawCornerArc(m_canvasFlyoutHighRes, x + r,     y + r,     r, thickness, borderColor, M_PI,       M_PI * 1.5);
   DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + r,     r, thickness, borderColor, M_PI * 1.5, M_PI * 2.0);
   DrawCornerArc(m_canvasFlyoutHighRes, x + r,     y + h - r, r, thickness, borderColor, M_PI * 0.5, M_PI);
   DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + h - r, r, thickness, borderColor, 0.0,        M_PI * 0.5);
  }

//+------------------------------------------------------------------+
//| Draw and composite the full flyout panel for the given category  |
//+------------------------------------------------------------------+
void CFlyoutPanel::DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool)
  {
   //--- Exit early if the category has no tools
   int nTools = ArraySize(m_categories[(int)cat].tools);
   if (nTools == 0) return;
   //--- Compute layout dimensions
   int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems);
   bool needsScroll = (nTools > m_flyoutMaxVisibleItems);
   int flyH   = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding;
   int totalW = m_flyoutWidth + m_flyoutPointerHeight;
   //--- Compute high-res canvas dimensions
   int ws = totalW * m_supersampleFactor, hs = flyH * m_supersampleFactor;
   bool ptrLeft = m_flyoutPointerOnLeft;
   //--- Compute scroll thumb height if scrolling is needed
   if (needsScroll)
     {
      int trackH = visibleTools * m_flyoutItemHeight;
      m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (double)m_flyoutMaxVisibleItems / nTools));
     }
   //--- Resize display and high-res canvases if flyout dimensions have changed
   if (m_canvasFlyout.Width() != totalW || m_canvasFlyout.Height() != flyH)
      m_canvasFlyout.Resize(totalW, flyH);
   if (m_canvasFlyoutHighRes.Width() != ws || m_canvasFlyoutHighRes.Height() != hs)
      m_canvasFlyoutHighRes.Resize(ws, hs);
   //--- Update the flyout chart object size to match
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_XSIZE, totalW);
   ObjectSetInteger(0, m_nameFlyout, OBJPROP_YSIZE, flyH);
   //--- Clear the high-res canvas to fully transparent
   m_canvasFlyoutHighRes.Erase(0x00000000);
   //--- Compute horizontal body offset and width at high resolution
   int bx = ptrLeft ? m_flyoutPointerHeight * m_supersampleFactor : 0;
   int bw = m_flyoutWidth * m_supersampleFactor;
   int br = m_panelCornerRadius * m_supersampleFactor;
   //--- Compute pointer tip Y and half-height at high resolution
   int ptrCY  = MathMax(br + m_flyoutPointerWidth * m_supersampleFactor + m_supersampleFactor,
                MathMin(hs - br - m_flyoutPointerWidth * m_supersampleFactor - m_supersampleFactor,
                        m_flyoutPointerLocalY * m_supersampleFactor));
   int ptrHHS = m_flyoutPointerWidth * m_supersampleFactor;
   //--- Compute pointer tip and base X based on pointer direction
   int tipX   = ptrLeft ? 0 : ws - 1, baseX = ptrLeft ? bx : bx + bw - 1;
   //--- Pack background and border colors
   uchar flyBgA     = (uchar)(255 * BackgroundOpacity);
   uint  fillARGB   = ColorToARGB(m_themeColors.flyoutBackground, flyBgA);
   uint  borderARGB = ColorToARGB(m_themeColors.flyoutBorder, 255);
   int   brdT       = BorderWidth * m_supersampleFactor;
   //--- Fill flyout body background with rounded corners
   FillRoundRectHR(m_canvasFlyoutHighRes, bx, 0, bw, hs, br, fillARGB);
   //--- Fill the pointer triangle background
   FillTriangleHR(m_canvasFlyoutHighRes, tipX, ptrCY, baseX, ptrCY - ptrHHS, baseX, ptrCY + ptrHHS, fillARGB);
   //--- Draw flyout body border and pointer edges if border is enabled
   if (BorderWidth > 0)
     {
      if (ptrLeft)
        {
         //--- Draw body border on all four sides
         DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB);
         //--- Erase the body-left gap where the pointer connects
         m_canvasFlyoutHighRes.FillRectangle(bx, ptrCY - ptrHHS, bx + brdT + m_supersampleFactor, ptrCY + ptrHHS, fillARGB);
         //--- Draw pointer triangle border edges
         DrawBorderEdge(m_canvasFlyoutHighRes, (double)bx,   (double)(ptrCY - ptrHHS), (double)tipX, (double)ptrCY,            brdT, borderARGB);
         DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX, (double)ptrCY,            (double)bx,   (double)(ptrCY + ptrHHS), brdT, borderARGB);
        }
      else
        {
         //--- Compute the body right boundary for the right-facing pointer
         int bodyRight = bx + bw;
         DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB);
         //--- Erase the body-right gap where the pointer connects
         m_canvasFlyoutHighRes.FillRectangle(bodyRight - brdT - m_supersampleFactor, ptrCY - ptrHHS, bodyRight, ptrCY + ptrHHS, fillARGB);
         //--- Draw pointer triangle border edges for right-facing pointer
         DrawBorderEdge(m_canvasFlyoutHighRes, (double)bodyRight, (double)(ptrCY - ptrHHS), (double)tipX,      (double)ptrCY,            brdT, borderARGB);
         DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX,      (double)ptrCY,            (double)bodyRight, (double)(ptrCY + ptrHHS), brdT, borderARGB);
        }
     }
   //--- Fill the flyout title strip background with rounded top corners
   color titleFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252';
   int   tbrd      = MathMax(brdT, m_supersampleFactor), innerTR = MathMax(0, br - tbrd);
   FillSelectiveRoundRectHR(m_canvasFlyoutHighRes, bx + tbrd, tbrd, bw - 2 * tbrd, titleH * m_supersampleFactor - tbrd, innerTR, ColorToARGB(titleFill, 255), true, true, false, false);
   //--- Square off the lower half of the title strip
   m_canvasFlyoutHighRes.FillRectangle(bx + tbrd, (titleH / 2) * m_supersampleFactor, bx + bw - tbrd - 1, titleH * m_supersampleFactor - 1, ColorToARGB(titleFill, 255));
   //--- Compute item clip boundaries
   int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight;
   //--- Draw item highlight backgrounds — use tmpHighRes when scrolling so highlights
   //--- never bleed above itemClipTop into the title strip after DownsampleCanvas
   if (needsScroll)
     {
      //--- Draw all scrolled item backgrounds onto a temporary HR canvas
      CCanvas tmpHighRes;
      tmpHighRes.Create("FlyoutTmpHR", ws, hs, COLOR_FORMAT_ARGB_NORMALIZE);
      tmpHighRes.Erase(0x00000000);
      for (int t = 0; t < nTools; t++)
        {
         //--- Compute scroll-adjusted item Y at high resolution
         int itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels) * m_supersampleFactor;
         //--- Skip items fully above or below the clip region
         if (itemY + (m_flyoutItemHeight - 2) * m_supersampleFactor <= itemClipTop * m_supersampleFactor) continue;
         if (itemY >= itemClipBot * m_supersampleFactor) continue;
         bool isActive  = (activeTool == m_categories[(int)cat].tools[t].toolType);
         bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat);
         int  itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor, padS = m_flyoutPadding * m_supersampleFactor;
         //--- Fill active item row background onto temp canvas
         if (isActive)
            FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255));
         //--- Fill hovered item row background onto temp canvas
         else if (isHovered)
            FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255));
         //--- Draw active indicator dot onto temp canvas
         if (isActive)
            tmpHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor,
                                   itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255));
        }
      //--- Blit only the clip region from temp onto the main HR canvas
      for (int y = itemClipTop * m_supersampleFactor; y < itemClipBot * m_supersampleFactor && y < hs; y++)
         for (int x = 0; x < ws; x++)
           {
            uint px = tmpHighRes.PixelGet(x, y);
            if (((px >> 24) & 0xFF) > 0) BlendPixelSet(m_canvasFlyoutHighRes, x, y, px);
           }
      tmpHighRes.Destroy();
     }
   else
     {
      //--- No scrolling — all items fit, draw directly onto the HR canvas
      for (int t = 0; t < visibleTools; t++)
        {
         bool isActive  = (activeTool == m_categories[(int)cat].tools[t].toolType);
         bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat);
         //--- Compute item Y at high resolution (no scroll offset needed)
         int  itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight) * m_supersampleFactor;
         int  itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor;
         int  padS  = m_flyoutPadding * m_supersampleFactor;
         //--- Fill active item row background
         if (isActive)
            FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255));
         //--- Fill hovered item row background
         else if (isHovered)
            FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255));
         //--- Draw active indicator dot
         if (isActive)
            m_canvasFlyoutHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor,
                                              itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255));
        }
     }
   //--- Downsample high-res canvas into the display-resolution flyout canvas
   DownsampleCanvas(m_canvasFlyout, m_canvasFlyoutHighRes, m_supersampleFactor);
   //--- Compute display-resolution body left offset
   int dispBx = ptrLeft ? m_flyoutPointerHeight : 0;
   //--- Draw horizontal separator below the title strip
   m_canvasFlyout.Line(dispBx + BorderWidth, titleH, dispBx + m_flyoutWidth - BorderWidth - 1, titleH, ColorToARGB(m_themeColors.flyoutBorder, 255));
   //--- Draw the uppercased category title text
   string titleStr = m_categories[(int)cat].categoryLabel;
   StringToUpper(titleStr);
   m_canvasFlyout.FontSet("Arial Bold", FlyoutTitleSize);
   m_canvasFlyout.TextOut(dispBx + m_flyoutPadding + 4, 6, titleStr, ColorToARGB(m_themeColors.flyoutTitleColor, 255));
   //--- Draw the tool count badge if the category has more than one tool
   if (nTools > 1)
     {
      string countStr = IntegerToString(nTools);
      m_canvasFlyout.FontSet("Arial", 15);
      int cw = m_canvasFlyout.TextWidth(countStr);
      //--- Right-align the count badge within the title strip
      m_canvasFlyout.TextOut(dispBx + m_flyoutWidth - m_flyoutPadding - cw - 4, 8, countStr, ColorToARGB(m_themeColors.flyoutTitleColor, 200));
     }
   //--- Create a temporary canvas for the icon and label text pass
   //--- Drawing directly onto m_canvasFlyout has no Y clipping; TextOut at scrolled
   //--- positions bleeds into the title strip. tmpText is seeded only for the clip
   //--- region and blitted back, so glyphs outside [itemClipTop, itemClipBot) are discarded
   CCanvas tmpText;
   tmpText.Create("FlyoutTmpText", m_canvasFlyout.Width(), m_canvasFlyout.Height(), COLOR_FORMAT_ARGB_NORMALIZE);
   tmpText.Erase(0x00000000);
   //--- Seed the clip region with existing flyout pixels as the drawing background
   for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++)
      for (int x = 0; x < m_canvasFlyout.Width(); x++)
         tmpText.PixelSet(x, y, m_canvasFlyout.PixelGet(x, y));
   //--- Draw icon glyphs and label text for each tool row onto the temp canvas
   for (int t = 0; t < nTools; t++)
     {
      //--- Compute scroll-adjusted display-resolution item Y
      int itemY = titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels;
      int itemH = m_flyoutItemHeight - 2;
      //--- Skip rows fully outside the clip region
      if (itemY + itemH <= itemClipTop || itemY >= itemClipBot) continue;
      bool isActive  = (activeTool == m_categories[(int)cat].tools[t].toolType);
      bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat);
      //--- Select icon and text colors based on state
      color iconColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.buttonIconColor);
      color textColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.flyoutTextColor);
      //--- Draw tool icon glyph onto the temp canvas
      tmpText.FontSet(m_categories[(int)cat].tools[t].iconFontName, FlyoutIconSize);
      string sym = CharToString(m_categories[(int)cat].tools[t].iconCharCode);
      int ih = tmpText.TextHeight(sym);
      tmpText.TextOut(dispBx + m_flyoutPadding + 8, itemY + (itemH - ih) / 2, sym, ColorToARGB(iconColor, 255));
      //--- Draw tool label text onto the temp canvas
      tmpText.FontSet("Arial", FlyoutLabelSize);
      int lh = tmpText.TextHeight(m_categories[(int)cat].tools[t].toolLabel);
      tmpText.TextOut(dispBx + m_flyoutPadding + 34, itemY + (itemH - lh) / 2,
                      m_categories[(int)cat].tools[t].toolLabel, ColorToARGB(textColor, 255));
     }
   //--- Blit only the clip region back onto the display canvas, discarding any out-of-bounds draws
   for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++)
      for (int x = 0; x < m_canvasFlyout.Width(); x++)
         m_canvasFlyout.PixelSet(x, y, tmpText.PixelGet(x, y));
   //--- Destroy the temporary canvas
   tmpText.Destroy();
   //--- Overlay the scroll thumb pill if hover or drag is active
   DrawFlyoutScrollPillOverlay(cat);
   //--- Flush the display canvas to the chart
   m_canvasFlyout.Update();
  }

//+------------------------------------------------------------------+
//| Draw the flyout scroll thumb pill overlay onto display canvas    |
//+------------------------------------------------------------------+
void CFlyoutPanel::DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat)
  {
   //--- Skip drawing if neither hovered nor dragging
   if (!m_isHoveredFlyoutScrollArea && !m_isFlyoutThumbDragging) return;
   if (cat == CAT_NONE) return;
   int nTools = ArraySize(m_categories[(int)cat].tools);
   //--- Skip if all items are visible and no scroll is needed
   if (nTools <= m_flyoutMaxVisibleItems) return;
   //--- Compute scroll track geometry
   int titleH = 26, itemsTop = titleH + m_flyoutPadding;
   int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
   m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (double)m_flyoutMaxVisibleItems / nTools));
   //--- Compute the thumb Y position from the current scroll fraction
   int    maxScrollPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
   double scrollPos   = (maxScrollPx > 0) ? (double)m_flyoutScrollPixels / maxScrollPx : 0.0;
   int    thumbY      = itemsTop + (int)(scrollPos * (trackH - m_flyoutScrollThumbHeight));
   //--- Compute scroll pill X position based on pointer direction
   int tw    = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0;
   int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2);
   //--- Select pill color and opacity based on interaction state
   color pillColor; uchar pillAlpha;
   if (m_isFlyoutThumbDragging)     { pillColor = m_themeColors.accentBarColor;        pillAlpha = 255; }
   else if (m_isHoveredFlyoutThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; }
   else                              { pillColor = m_themeColors.scrollArrowColor;      pillAlpha = 180; }
   uint thumbARGB = ColorToARGB(pillColor, pillAlpha);
   //--- Create a temporary high-res canvas for the pill shape
   int    pws = tw * m_supersampleFactor, phs = m_flyoutScrollThumbHeight * m_supersampleFactor;
   CCanvas pillHR;
   pillHR.Create("FlyoutPillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE);
   pillHR.Erase(0x00000000);
   //--- Fill the pill with a fully rounded rect at high resolution
   FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB);
   //--- Downsample the pill and blend it onto the flyout display canvas
   for (int py = 0; py < m_flyoutScrollThumbHeight; py++)
      for (int px = 0; px < tw; px++)
        {
         //--- Accumulate channel sums across the high-res sample block
         double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0;
         for (int dy = 0; dy < m_supersampleFactor; dy++)
            for (int dx = 0; dx < m_supersampleFactor; dx++)
              {
               int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy;
               if (sx >= pws || sy >= phs) continue;
               uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF);
               sumA += a;
               if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; }
              }
         //--- Compute averaged output alpha and blend onto the display canvas
         int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2);
         if (fa > 0 && wc > 0)
            BlendPixelSet(m_canvasFlyout, thinX + px, thumbY + py,
               ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc));
        }
   //--- Destroy the temporary high-res pill canvas
   pillHR.Destroy();
  }

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

Далее метод "HideFlyout" сбрасывает значение элемента при наведении курсора и смещение прокрутки, очищает флаги при наведении курсора на прокрутку, скрывает объект графика выдвижной панели, устанавливая его временные рамки в OBJ_NO_PERIODS, и помечает всплывающее окно как скрытое. Метод "HitTestOverFlyout" считывает положение и размер объекта графика выдвижной панели с помощью ObjectGetInteger, вычисляет локальные координаты и возвращает значение, указывающее, находится ли курсор мыши в пределах его границ. Метод "HitTestFlyoutItem" вычисляет, над каким рядом инструментов находится курсор, учитывая высоту заголовка, отступы, смещение прокрутки и границы клипов, исключая при этом щелчки по столбцу с ползуном прокрутки.

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

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

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

POP OUT ILLUSTRATION

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

Расширение рендерера боковой панели с помощью состояний наведения курсора и ползунка прокрутки

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

//+------------------------------------------------------------------+
//| CLASS 7 — Draw and composite all sidebar visual elements         |
//+------------------------------------------------------------------+
class CSidebarRenderer : public CFlyoutPanel
  {
protected:
   ENUM_CATEGORY m_hoveredCategory;      // Currently hovered category button, or CAT_NONE
   bool          m_isCloseButtonHovered; // Flag indicating the close button is hovered
   bool          m_isThemeButtonHovered; // Flag indicating the theme toggle button is hovered
   bool          m_isGripAreaHovered;    // Flag indicating the drag-grip area is hovered

protected:
   //--- Draw and composite the full sidebar onto its canvas
   void DrawSidebar(TOOL_TYPE activeTool);
   //--- Draw the header grip strip at high resolution
   void DrawHeaderStripHR(int canvasW, int canvasH);
   //--- Draw a single category button with active, hover, and dot states at high resolution
   void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot);
   //--- Draw icon glyphs and separator lines onto the display canvas
   void DrawSidebarIconLabels(TOOL_TYPE activeTool);
   //--- Draw the sidebar scroll thumb pill overlay onto the display canvas
   void DrawSidebarScrollPillOverlay();
  };

//+------------------------------------------------------------------+
//| Draw the sidebar scroll thumb pill overlay onto display canvas   |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebarScrollPillOverlay()
  {
   //--- Skip drawing if scrolling is not needed or neither hovered nor dragging
   if (CalcSidebarMaxScrollPixels() <= 0 || (!m_isHoveredSidebarScrollArea && !m_isSidebarThumbDragging)) return;
   //--- Compute scroll track geometry
   int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels();
   m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels()));
   int maxPx = CalcSidebarMaxScrollPixels();
   //--- Compute the thumb Y position from the current scroll fraction
   double pos    = (maxPx > 0) ? (double)m_sidebarScrollPixels / maxPx : 0.0;
   int    thumbY = trackY + (int)(pos * (trackH - m_sidebarScrollThumbHeight));
   //--- Compute scroll pill X position based on snap state
   int tw    = m_sidebarScrollThinWidth;
   int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2;
   //--- Select pill color and opacity based on interaction state
   color pillColor; uchar pillAlpha;
   if (m_isSidebarThumbDragging)     { pillColor = m_themeColors.accentBarColor;        pillAlpha = 255; }
   else if (m_isHoveredSidebarThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; }
   else                               { pillColor = m_themeColors.scrollArrowColor;      pillAlpha = 180; }
   uint thumbARGB = ColorToARGB(pillColor, pillAlpha);
   //--- Create a temporary high-res canvas for the pill shape
   int    pws = tw * m_supersampleFactor, phs = m_sidebarScrollThumbHeight * m_supersampleFactor;
   CCanvas pillHR;
   pillHR.Create("SB_PillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE);
   pillHR.Erase(0x00000000);
   //--- Fill the pill with a fully rounded rect at high resolution
   FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB);
   //--- Downsample the pill and blend it onto the sidebar display canvas
   for (int py = 0; py < m_sidebarScrollThumbHeight; py++)
      for (int px = 0; px < tw; px++)
        {
         //--- Accumulate channel sums across the high-res sample block
         double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0;
         for (int dy = 0; dy < m_supersampleFactor; dy++)
            for (int dx = 0; dx < m_supersampleFactor; dx++)
              {
               int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy;
               if (sx >= pws || sy >= phs) continue;
               uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF);
               sumA += a;
               if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; }
              }
         //--- Compute averaged output alpha and blend onto the display canvas
         int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2);
         if (fa > 0 && wc > 0)
            BlendPixelSet(m_canvasSidebar, thinX + px, thumbY + py,
               ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc));
        }
   //--- Destroy the temporary high-res pill canvas
   pillHR.Destroy();
  }

Здесь мы объявляем класс "CSidebarRenderer", который теперь наследует от "CFlyoutPanel" вместо "CSidebarLayout", как в предыдущей части, предоставляя ему доступ ко всей системе выдвижных панелей. Мы добавляем четыре новых защищенных члена, отслеживающих, какая кнопка категории находится под курсором и находится ли кнопка закрытия, кнопка темы или область захвата в данный момент под курсором. Метод "DrawSidebar" теперь принимает в качестве параметра тип активного инструмента, что позволяет определить, какую кнопку категории следует выделить. Метод "DrawCategoryButtonHR" получает логические параметры active и hovered в дополнение к существующему флагу dot, что позволяет отображать три визуальных состояния. Мы также добавляем метод "DrawSidebarScrollPillOverlay" и сохраняем существующие методы отрисовки заголовка и меток значков, которые теперь принимают тип активного инструмента для отрисовки с учетом состояния.

Метод "DrawSidebarScrollPillOverlay" отображает тонкий закругленный индикатор прокрутки на краю боковой панели, когда список категорий выходит за пределы области прокрутки и пользователь наводит курсор или перетаскивает область прокрутки. Мы вычисляем положение ползунка относительно текущей доли прокрутки и высоты дорожки, позиционируем его на левом или правом краю в зависимости от состояния привязки и выбираем цвет ползунка в зависимости от того, находится ли ползунок в состоянии покоя, под курсором или перетаскивается. Далее мы создаём временный холст высокого разрешения, заполняем его скруглённым прямоугольником, вручную уменьшаем разрешение пиксель за пикселем и накладываем полученный результат на холст боковой панели с помощью функции "BlendPixelSet". Это придаёт ползунку прокрутки такое же сглаживание, как и остальной части боковой панели. После этого у нас есть все необходимые элементы. Теперь мы определим новый класс для обработки всех событий мыши и графиков. Сначала давайте объявим его.

Представляем класс обработчика событий графика

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

//+------------------------------------------------------------------+
//| CLASS 8 — Route and handle all chart interaction events          |
//+------------------------------------------------------------------+
class CChartEventHandler : public CSidebarRenderer
  {
protected:
   int m_previousMouseButtonState; // Mouse button state recorded on the previous move event

protected:
   //--- Dispatch an incoming chart event to the appropriate handler
   void RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool);
   //--- Handle CHARTEVENT_CHART_CHANGE to reflow layout on resize
   void OnChartChangeEvent(TOOL_TYPE activeTool);
   //--- Handle CHARTEVENT_MOUSE_WHEEL to scroll sidebar or flyout
   void OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool);
   //--- Handle CHARTEVENT_MOUSE_MOVE to process all mouse interactions
   void OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool);
   //--- Move the panel to follow the mouse during a drag operation
   void HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool);
   //--- Finalize a panel drag by snapping to the nearest edge
   void HandlePanelDragRelease(TOOL_TYPE activeTool);
   //--- Resize the panel bottom edge as the mouse moves
   void HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool);
   //--- Scroll the sidebar by moving the scroll thumb
   void HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool);
   //--- Finalize a sidebar scroll thumb drag
   void HandleSidebarThumbRelease(TOOL_TYPE activeTool);
   //--- Scroll the flyout list by moving the flyout scroll thumb
   void HandleFlyoutThumbDrag(int mouseX, int mouseY);
   //--- Finalize a flyout scroll thumb drag
   void HandleFlyoutThumbRelease();
   //--- Recompute all hover state flags and trigger redraws as needed
   void UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool);
   //--- Handle a mouse button-down event within the sidebar or flyout
   void HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool);
  };

Здесь мы объявляем класс "CChartEventHandler", который наследует от "CSidebarRenderer" и вводит единственную переменную состояния, отслеживающую предыдущее состояние кнопки мыши для обнаружения новых переходов при щелчке. Класс объявляет четырнадцать защищенных методов, которые в совокупности обрабатывают все взаимодействия пользователя, поддерживаемые боковой панелью.

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

Для перемещения панели функция "HandlePanelDragMove" зафиксирует положение панели в пределах границ графика и переместит выдвижную панель вслед за ним, а функция "HandlePanelDragRelease" завершит перетаскивание, вызвав логику привязки к краю и пересчитав компоновку. Метод "HandleBottomResizeDrag" будет регулировать высоту панели по мере перемещения мыши, ограничивая ее диапазоном между минимальным и естественным значениями. Для взаимодействия с прокруткой методы "HandleSidebarThumbDrag" и "HandleFlyoutThumbDrag" сопоставляют изменения положения мыши со смещениями прокрутки для соответствующих списков, с соответствующими методами отпускания, которые очищают состояние перетаскивания.

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

//+------------------------------------------------------------------+
//| Dispatch an incoming chart event to the appropriate handler      |
//+------------------------------------------------------------------+
void CChartEventHandler::RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool)
  {
   //--- Forward chart change events to the resize/reflow handler
   if (id == CHARTEVENT_CHART_CHANGE) { OnChartChangeEvent(activeTool); return; }
   //--- Forward mouse wheel events to the scroll handler
   if (id == CHARTEVENT_MOUSE_WHEEL)  { OnMouseWheelEvent((int)(short)lp, (int)(short)(lp >> 16), (int)dp, activeTool); return; }
   //--- Forward mouse move events to the full mouse interaction handler
   if (id == CHARTEVENT_MOUSE_MOVE)     OnMouseMoveEvent((int)lp, (int)dp, (int)sp, activeTool);
  }

//+------------------------------------------------------------------+
//| Handle chart change event to reflow layout on resize             |
//+------------------------------------------------------------------+
void CChartEventHandler::OnChartChangeEvent(TOOL_TYPE activeTool)
  {
   //--- Reset all drag and thumb states on chart geometry change
   m_previousMouseButtonState = 0;
   m_isPanelDragging          = false;
   m_isResizingBottomEdge     = false;
   m_isSidebarThumbDragging   = false;
   m_isFlyoutThumbDragging    = false;
   //--- Restore chart mouse scroll in case it was locked during a drag
   ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
   //--- Reposition and reflow snapped panels only
   if (m_snapState != SNAP_FLOAT)
     {
      //--- Recalculate snapped panel X position from chart width
      int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
      m_panelX = (m_snapState == SNAP_RIGHT) ? chartW - m_sidebarWidth : 0;
      ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
      //--- Clamp snapped height override within the new chart height
      if (m_snappedSidebarHeight > 0)
         m_snappedSidebarHeight = MathMin(m_snappedSidebarHeight, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - m_panelY - 8);
      //--- Recompute layout and resize canvases
      CalcSidebarHeight();
      ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight);
      DrawSidebar(activeTool);
      //--- Reposition the flyout if it is currently visible
      if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool);
     }
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Handle mouse wheel event to scroll sidebar or flyout list        |
//+------------------------------------------------------------------+
void CChartEventHandler::OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool)
  {
   int lx, ly, flx, fly;
   bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly);
   bool overFlyout  = HitTestOverFlyout(mouseX, mouseY, flx, fly);
   //--- Scroll the sidebar when the wheel is over the sidebar and it is scrollable
   if (overSidebar && m_sidebarMaxVisibleCats < CAT_COUNT)
     {
      //--- Lock chart scroll to prevent chart panning while scrolling the sidebar
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
      int step = MathMax(1, MouseScrollSpeed);
      //--- Scroll down on negative delta, up on positive
      m_sidebarScrollPixels = MathMax(0, MathMin(m_sidebarScrollPixels + ((wheelDelta < 0) ? step : -step), CalcSidebarMaxScrollPixels()));
      HideFlyout();
      DrawSidebar(activeTool);
      ChartRedraw();
      return;
     }
   //--- Scroll the flyout list when the wheel is over the flyout and it is scrollable
   if (overFlyout && m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE)
     {
      int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
      if (nTools > m_flyoutMaxVisibleItems)
        {
         //--- Lock chart scroll to prevent chart panning while scrolling the flyout
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
         //--- Scroll flyout list by the configured step amount
         m_flyoutScrollPixels = MathMax(0, MathMin(m_flyoutScrollPixels + ((wheelDelta < 0) ? MathMax(1, MouseScrollSpeed) : -MathMax(1, MouseScrollSpeed)), maxPx));
         DrawFlyoutForCategory(m_flyoutActiveCat, activeTool);
         ChartRedraw();
        }
      return;
     }
   //--- Restore chart scroll when the wheel is not over any panel
   ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
  }

//+------------------------------------------------------------------+
//| Move the panel to follow the mouse during a drag operation       |
//+------------------------------------------------------------------+
void CChartEventHandler::HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool)
  {
   //--- Clamp panel position within chart bounds
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   m_panelX = MathMax(0, MathMin(chartW - m_sidebarWidth,  mouseX - m_dragOffsetX));
   m_panelY = MathMax(0, MathMin(chartH - m_sidebarHeight, mouseY - m_dragOffsetY));
   //--- Update the chart object position to follow the mouse
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   //--- Reposition the flyout to follow the sidebar if visible
   if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool);
   DrawSidebar(activeTool);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Finalize panel drag by snapping to the nearest edge              |
//+------------------------------------------------------------------+
void CChartEventHandler::HandlePanelDragRelease(TOOL_TYPE activeTool)
  {
   //--- Clear drag state flag
   m_isPanelDragging = false;
   //--- Attempt to snap the panel to a chart edge
   TrySnapToEdge();
   //--- Recompute layout after potential snap state change
   CalcSidebarHeight();
   ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight);
   DrawSidebar(activeTool);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Resize the panel bottom edge as the mouse moves                  |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool)
  {
   //--- Compute vertical mouse delta from drag start
   int dy     = mouseY - m_bottomResizeDragStartY;
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //--- Compute natural height for all buttons and height clamp limits
   int naturalH = m_headerGripHeight + 8 + CAT_COUNT * (m_categoryButtonSize + 6) - 6 + 10;
   int minH     = m_headerGripHeight + 8 + 10 + 3 * (m_categoryButtonSize + 6) - 6;
   int maxH     = (m_snapState != SNAP_FLOAT) ? MathMin(naturalH, chartH - m_panelY - 8) : chartH - m_panelY - 8;
   //--- Compute the new panel height clamped within valid bounds
   int newH = MathMax(minH, MathMin(maxH, m_bottomResizeStartHeight + dy));
   //--- Apply the new height if it has changed
   if (newH != m_sidebarHeight)
     {
      //--- Store as snapped height override for non-floating panels
      if (m_snapState != SNAP_FLOAT) m_snappedSidebarHeight = newH; else m_sidebarHeight = newH;
      CalcSidebarHeight();
      ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight);
      DrawSidebar(activeTool);
      ChartRedraw();
     }
  }

//+------------------------------------------------------------------+
//| Scroll the sidebar by moving the scroll thumb                    |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool)
  {
   //--- Compute the available travel distance for the thumb
   int trackH = CalcSidebarViewportPixels(), travel = trackH - m_sidebarScrollThumbHeight;
   if (travel > 0)
     {
      //--- Map mouse delta to scroll offset delta
      int dy    = mouseY - m_sidebarThumbDragStartY;
      int maxPx = CalcSidebarMaxScrollPixels();
      int newPx = MathMax(0, MathMin(maxPx, m_sidebarThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx)));
      //--- Apply the new scroll offset if it has changed
      if (newPx != m_sidebarScrollPixels)
        {
         m_sidebarScrollPixels = newPx;
         HideFlyout();
         DrawSidebar(activeTool);
         ChartRedraw();
        }
     }
  }

//+------------------------------------------------------------------+
//| Finalize a sidebar scroll thumb drag                             |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleSidebarThumbRelease(TOOL_TYPE activeTool)
  {
   //--- Clear the sidebar thumb dragging flag and redraw
   m_isSidebarThumbDragging = false;
   DrawSidebar(activeTool);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Scroll the flyout list by moving the flyout scroll thumb         |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleFlyoutThumbDrag(int mouseX, int mouseY)
  {
   //--- Exit if no flyout category is active
   if (m_flyoutActiveCat == CAT_NONE) return;
   int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
   if (nTools <= m_flyoutMaxVisibleItems) return;
   //--- Compute the available travel distance for the flyout thumb
   int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight, travel = trackH - m_flyoutScrollThumbHeight;
   if (travel > 0)
     {
      //--- Map mouse delta to flyout scroll offset delta
      int dy    = mouseY - m_flyoutThumbDragStartY;
      int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
      int newPx = MathMax(0, MathMin(maxPx, m_flyoutThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx)));
      //--- Apply the new flyout scroll offset if it has changed
      if (newPx != m_flyoutScrollPixels)
        {
         m_flyoutScrollPixels = newPx;
         DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE);
         ChartRedraw();
        }
     }
  }

//+------------------------------------------------------------------+
//| Finalize a flyout scroll thumb drag                              |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleFlyoutThumbRelease()
  {
   //--- Clear the flyout thumb dragging flag and redraw
   m_isFlyoutThumbDragging = false;
   DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Recompute all hover states and trigger redraws as needed         |
//+------------------------------------------------------------------+
void CChartEventHandler::UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout,
                                               int lx, int ly, int flx, int fly, TOOL_TYPE activeTool)
  {
   //--- Snapshot all hover flags before updating for change detection
   ENUM_CATEGORY prevHovCat  = m_hoveredCategory;
   int           prevHovItem = m_hoveredFlyoutItem;
   bool prevClose   = m_isCloseButtonHovered,  prevTheme  = m_isThemeButtonHovered;
   bool prevGrip    = m_isGripAreaHovered,      prevSBA    = m_isHoveredSidebarScrollArea;
   bool prevFSA     = m_isHoveredFlyoutScrollArea, prevBR = m_isBottomResizeHovered;
   bool prevSbTh    = m_isHoveredSidebarThumb,  prevFlyTh = m_isHoveredFlyoutThumb;
   //--- Clear all hover flags before recomputing
   m_isCloseButtonHovered = m_isThemeButtonHovered = m_isGripAreaHovered = false;
   m_isBottomResizeHovered = m_isHoveredSidebarScrollArea = m_isHoveredSidebarThumb = false;
   m_isHoveredFlyoutScrollArea = m_isHoveredFlyoutThumb = false;
   //--- Recompute sidebar hover states when the mouse is over the sidebar
   if (overSidebar)
     {
      m_hoveredCategory       = HitTestCategoryButton(lx, ly);
      m_isCloseButtonHovered  = HitTestOverCloseButton(lx, ly);
      m_isThemeButtonHovered  = HitTestOverThemeButton(lx, ly);
      m_isGripAreaHovered     = HitTestOverGripArea(lx, ly);
      m_isBottomResizeHovered = HitTestOverBottomResizeGrip(lx, ly);
      //--- Recompute scroll thumb hover state if the sidebar is scrollable
      if (CalcSidebarMaxScrollPixels() > 0)
        {
         int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels();
         m_isHoveredSidebarScrollArea = (ly >= trackY && ly <= trackY + trackH);
         if (m_isHoveredSidebarScrollArea)
           {
            //--- Check if the mouse is over the narrow scroll pill column
            int tw = m_sidebarScrollThinWidth, thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2;
            if (lx >= thinX - 4 && lx <= thinX + tw + 4)
              {
               //--- Compute thumb Y position from scroll fraction and check hit
               int maxPx    = CalcSidebarMaxScrollPixels();
               int sliderY  = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight));
               m_isHoveredSidebarThumb = (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight);
              }
           }
        }
     }
   //--- Clear hovered category if not over the flyout either
   else if (!overFlyout) m_hoveredCategory = CAT_NONE;
   //--- Recompute flyout hover states when the mouse is over the flyout
   if (overFlyout)
     {
      m_hoveredFlyoutItem = HitTestFlyoutItem(flx, fly);
      if (m_hoveredFlyoutItem < 0) m_hoveredFlyoutItem = -1;
      //--- Mark flyout scroll area as hovered if it is scrollable
      m_isHoveredFlyoutScrollArea = m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE &&
                                    (ArraySize(m_categories[(int)m_flyoutActiveCat].tools) > m_flyoutMaxVisibleItems);
      if (m_isHoveredFlyoutScrollArea)
        {
         int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
         int titleH = 26, itemsTop = titleH + m_flyoutPadding;
         int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
         int tw     = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0;
         //--- Compute the scroll pill X column based on pointer direction
         int thinX  = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2);
         if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH)
           {
            //--- Compute flyout thumb Y position from scroll fraction and check hit
            int maxPx   = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
            int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight));
            m_isHoveredFlyoutThumb = (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight);
           }
        }
     }
   //--- Clear flyout item hover when not over either panel
   else if (!overSidebar) { m_hoveredFlyoutItem = -1; m_isHoveredFlyoutScrollArea = false; }
   //--- Show the flyout when hovering a new category button
   if (overSidebar && m_hoveredCategory != CAT_NONE &&
       !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered)
     {
      if (m_hoveredCategory != m_flyoutActiveCat) ShowFlyout(m_hoveredCategory, activeTool);
     }
   //--- Hide the flyout when the mouse leaves both panels
   else if (!overFlyout && m_isFlyoutVisible)
     {
      //--- Allow brief transition across the gap between sidebar and flyout
      bool transitEdge = false;
      if (overSidebar)
        {
         int margin = m_sidebarWidth / 4;
         transitEdge = (m_snapState == SNAP_LEFT)  ? (lx >= m_sidebarWidth - margin) :
                       (m_snapState == SNAP_RIGHT) ? (lx <= margin) :
                       (m_flyoutPointerOnLeft ? (lx >= m_sidebarWidth - margin) : (lx <= margin));
        }
      if (!transitEdge) { HideFlyout(); ChartRedraw(); }
     }
   //--- Trigger a redraw only when any hover state has changed
   bool changed = (prevHovCat  != m_hoveredCategory  || prevHovItem != m_hoveredFlyoutItem ||
                   prevClose   != m_isCloseButtonHovered || prevTheme != m_isThemeButtonHovered ||
                   prevGrip    != m_isGripAreaHovered || prevSBA    != m_isHoveredSidebarScrollArea ||
                   prevFSA     != m_isHoveredFlyoutScrollArea || prevBR != m_isBottomResizeHovered ||
                   prevSbTh    != m_isHoveredSidebarThumb || prevFlyTh != m_isHoveredFlyoutThumb);
   if (changed)
     {
      DrawSidebar(activeTool);
      if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, activeTool);
      ChartRedraw();
     }
  }

//+------------------------------------------------------------------+
//| Handle a mouse button-down event within the sidebar or flyout    |
//+------------------------------------------------------------------+
void CChartEventHandler::HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout,
                                               int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool)
  {
   //--- Handle sidebar scroll thumb and track clicks
   if (overSidebar && CalcSidebarMaxScrollPixels() > 0)
     {
      int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(), tw = m_sidebarScrollThinWidth;
      int thinX  = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2;
      if (lx >= thinX - 4 && lx <= thinX + tw + 4 && ly >= trackY && ly <= trackY + trackH)
        {
         int maxPx   = CalcSidebarMaxScrollPixels();
         int sliderY = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight));
         //--- Begin thumb drag if the click is on the thumb
         if (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight)
           {
            m_isSidebarThumbDragging      = true;
            m_sidebarThumbDragStartY      = mouseY;
            m_sidebarThumbDragStartPixels = m_sidebarScrollPixels;
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
            HideFlyout();
            DrawSidebar(activeTool);
            ChartRedraw();
           }
         else
           {
            //--- Page-scroll the sidebar by one button step when clicking the track
            int step = m_categoryButtonSize + m_categoryButtonPadding;
            m_sidebarScrollPixels = MathMax(0, MathMin(maxPx, m_sidebarScrollPixels + ((ly < sliderY) ? -step : step)));
            HideFlyout();
            DrawSidebar(activeTool);
            ChartRedraw();
           }
         return;
        }
     }
   //--- Handle flyout scroll thumb and track clicks
   if (overFlyout && m_flyoutActiveCat != CAT_NONE)
     {
      int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
      if (nTools > m_flyoutMaxVisibleItems)
        {
         int titleH = 26, itemsTop = titleH + m_flyoutPadding;
         int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
         int tw     = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0;
         int thinX  = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2);
         if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH)
           {
            int maxPx   = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight;
            int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight));
            //--- Begin flyout thumb drag if the click is on the thumb
            if (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight)
              {
               m_isFlyoutThumbDragging      = true;
               m_flyoutThumbDragStartY      = mouseY;
               m_flyoutThumbDragStartPixels = m_flyoutScrollPixels;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
               DrawFlyoutForCategory(m_flyoutActiveCat, activeTool);
               ChartRedraw();
              }
            else
              {
               //--- Page-scroll the flyout by one item height when clicking the track
               m_flyoutScrollPixels = MathMax(0, MathMin(maxPx, m_flyoutScrollPixels + ((fly < sliderY) ? -m_flyoutItemHeight : m_flyoutItemHeight)));
               DrawFlyoutForCategory(m_flyoutActiveCat, activeTool);
               ChartRedraw();
              }
            return;
           }
        }
     }
   //--- Begin panel drag when clicking the grip area
   if (overSidebar && HitTestOverGripArea(lx, ly) && !m_isCloseButtonHovered && !m_isThemeButtonHovered)
     {
      m_isPanelDragging = true;
      m_dragOffsetX     = lx;
      m_dragOffsetY     = ly;
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
      HideFlyout();
      return;
     }
   //--- Begin bottom resize drag when clicking the resize grip
   if (overSidebar && HitTestOverBottomResizeGrip(lx, ly))
     {
      m_isResizingBottomEdge      = true;
      m_bottomResizeDragStartY    = mouseY;
      m_bottomResizeStartHeight   = m_sidebarHeight;
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
      HideFlyout();
      return;
     }
   //--- Remove the indicator when the close button is clicked
   if (overSidebar && m_isCloseButtonHovered) { ExpertRemove(); return; }
  }

//+------------------------------------------------------------------+
//| Handle CHARTEVENT_MOUSE_MOVE to process all mouse interactions   |
//+------------------------------------------------------------------+
void CChartEventHandler::OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool)
  {
   int lx, ly, flx, fly;
   bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly);
   bool overFlyout  = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly);
   //--- Handle active drag and thumb operations first, before hover or click logic
   if (m_isPanelDragging        && mouseButtons == 1) { HandlePanelDragMove(mouseX, mouseY, activeTool);    m_previousMouseButtonState = mouseButtons; return; }
   if (m_isPanelDragging        && mouseButtons == 0) { HandlePanelDragRelease(activeTool);                 m_previousMouseButtonState = mouseButtons; return; }
   if (m_isResizingBottomEdge   && mouseButtons == 1) { HandleBottomResizeDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; }
   if (m_isResizingBottomEdge   && mouseButtons == 0) { m_isResizingBottomEdge = false;                     m_previousMouseButtonState = mouseButtons; return; }
   if (m_isSidebarThumbDragging && mouseButtons == 1) { HandleSidebarThumbDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; }
   if (m_isSidebarThumbDragging && mouseButtons == 0) { HandleSidebarThumbRelease(activeTool);              m_previousMouseButtonState = mouseButtons; return; }
   if (m_isFlyoutThumbDragging  && mouseButtons == 1) { HandleFlyoutThumbDrag(mouseX, mouseY);              m_previousMouseButtonState = mouseButtons; return; }
   if (m_isFlyoutThumbDragging  && mouseButtons == 0) { HandleFlyoutThumbRelease();                         m_previousMouseButtonState = mouseButtons; return; }
   //--- Recompute all hover states for the current mouse position
   UpdateAllHoverStates(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool);
   //--- Manage chart scroll lock: lock when over any panel, unlock otherwise
   bool overAny = overSidebar || overFlyout;
   if (!m_isSidebarThumbDragging && !m_isPanelDragging && !m_isResizingBottomEdge && !m_isFlyoutThumbDragging)
      ChartSetInteger(0, CHART_MOUSE_SCROLL, !overAny);
   //--- Detect a fresh left-button press and route to the click-down handler
   if (mouseButtons == 1 && m_previousMouseButtonState == 0)
      HandleMouseClickDown(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool);
   //--- Record button state for next event comparison
   m_previousMouseButtonState = mouseButtons;
  }

Сначала реализуем метод "RouteChartEvent" в качестве диспетчера верхнего уровня. Он проверяет идентификатор события и перенаправляет события изменения графика в метод "OnChartChangeEvent", события прокрутки колесом мыши в метод "OnMouseWheelEvent" с извлеченными координатами и дельтой, а события перемещения мыши в метод "OnMouseMoveEvent".

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

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

Для перемещения панелей "HandlePanelDragMove" ограничивает положение панели границами графика с помощью MathMax и MathMin, обновляет положение объекта графика, перемещает выдвижную панель, если она видима, и перерисовывает его. "HandlePanelDragRelease" сбрасывает флаг перетаскивания, вызывает "TrySnapToEdge", пересчитывает компоновку и изменяет размер холстов. "HandleBottomResizeDrag" вычисляет разницу от начала перетаскивания, ограничивает новую высоту между минимальным и естественным пределами, сохраняет ее как переопределение высоты для закрепленных панелей и перестраивает компоновку.

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

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

Метод "HandleMouseClickDown" обрабатывает новые нажатия левой кнопки мыши, сначала проверяя столбцы прокрутки боковой панели и выдвижной панели на предмет перетаскивания ползунка или отслеживания прокрутки страниц. Затем инициирует перетаскивание панели из области захвата, изменение размера нижней части из области изменения размера или вызывает ExpertRemove при нажатии кнопки закрытия.

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

CHART EVENT ILLUSTRATION

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

Представляем класс графического движка

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

//+------------------------------------------------------------------+
//| CLASS 9 — Place chart drawing objects from tool interactions     |
//+------------------------------------------------------------------+
class CDrawingEngine : public CChartEventHandler
  {
protected:
   int      m_drawnObjectCounter;   // Running counter used to generate unique object names
   int      m_toolDrawingClickCount;// Number of chart clicks recorded for the current tool placement
   datetime m_drawPoint1Time;       // Chart time of the first placement click
   datetime m_drawPoint2Time;       // Chart time of the second placement click
   double   m_drawPoint1Price;      // Chart price of the first placement click
   double   m_drawPoint2Price;      // Chart price of the second placement click

protected:
   //--- Generate a unique name for the next drawing object
   string MakeUniqueObjectName();
   //--- Process a chart click and dispatch to the appropriate object creator
   void   HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction);
   //--- Create a chart object that requires a single placement click
   void   CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType);
   //--- Create a chart object that requires two placement clicks
   void   CreateTwoClickObject(int sub, TOOL_TYPE toolType);
   //--- Create a chart object that requires three placement clicks
   void   CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType);
  };

//+------------------------------------------------------------------+
//| Generate a unique name for the next drawing object               |
//+------------------------------------------------------------------+
string CDrawingEngine::MakeUniqueObjectName()
  {
   //--- Increment the counter and combine it with the current time for uniqueness
   m_drawnObjectCounter++;
   return "ToolsPalette_Drawing_" + IntegerToString(m_drawnObjectCounter) + "_" + IntegerToString((int)TimeCurrent());
  }

//+------------------------------------------------------------------+
//| Process a chart click and dispatch to the appropriate creator    |
//+------------------------------------------------------------------+
void CDrawingEngine::HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction)
  {
   //--- Convert screen coordinates to chart time and price
   datetime barTime; double barPrice; int sub;
   if (!ChartXYToTimePrice(m_chartId, mouseX, mouseY, sub, barTime, barPrice)) return;
   //--- Exit if the active tool requires no clicks
   int clicksNeeded = GetRequiredClickCount(activeTool);
   if (clicksNeeded <= 0) return;
   //--- Increment the click count for the ongoing placement sequence
   m_toolDrawingClickCount++;
   if (m_toolDrawingClickCount == 1)
     {
      //--- Record the first anchor point
      m_drawPoint1Time  = barTime;
      m_drawPoint1Price = barPrice;
      //--- Create object immediately for single-click tools
      if (clicksNeeded == 1)
        {
         CreateSingleClickObject(sub, barTime, barPrice, activeTool);
         m_toolDrawingClickCount = 0;
         activeTool              = TOOL_NONE;
         instruction             = "";
        }
      else instruction = "Click second point for " + GetToolLabel(activeTool) + ".";
     }
   else if (m_toolDrawingClickCount == 2)
     {
      //--- Record the second anchor point
      m_drawPoint2Time  = barTime;
      m_drawPoint2Price = barPrice;
      //--- Create object immediately for two-click tools
      if (clicksNeeded == 2)
        {
         CreateTwoClickObject(sub, activeTool);
         m_toolDrawingClickCount = 0;
         activeTool              = TOOL_NONE;
         instruction             = "";
        }
      else instruction = "Click third point for " + GetToolLabel(activeTool) + ".";
     }
   else if (m_toolDrawingClickCount == 3)
     {
      //--- Create object for three-click tools using all three recorded anchor points
      CreateThreeClickObject(sub, barTime, barPrice, activeTool);
      m_toolDrawingClickCount = 0;
      activeTool              = TOOL_NONE;
      instruction             = "";
     }
  }

//+------------------------------------------------------------------+
//| Create a chart object that requires a single placement click     |
//+------------------------------------------------------------------+
void CDrawingEngine::CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType)
  {
   string name = MakeUniqueObjectName(); bool ok = false;
   switch (toolType)
     {
      //--- Create a horizontal line at the clicked price
      case TOOL_HLINE:
         ok = ObjectCreate(m_chartId, name, OBJ_HLINE, 0, 0, p);
         if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); }
         break;
      //--- Create a vertical line at the clicked time
      case TOOL_VLINE:
         ok = ObjectCreate(m_chartId, name, OBJ_VLINE, 0, t, 0);
         if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); }
         break;
      //--- Create a text label at the clicked position
      case TOOL_TEXT:
         ok = ObjectCreate(m_chartId, name, OBJ_TEXT, sub, t, p);
         if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, "Text"); ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(m_chartId, name, OBJPROP_FONTSIZE, 10); }
         break;
      //--- Create an arrow-up annotation at the clicked position
      case TOOL_ARROW_UP:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_UP, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime);
         break;
      //--- Create an arrow-down annotation at the clicked position
      case TOOL_ARROW_DOWN:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_DOWN, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed);
         break;
      //--- Create a thumbs-up annotation at the clicked position
      case TOOL_THUMB_UP:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_UP, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime);
         break;
      //--- Create a thumbs-down annotation at the clicked position
      case TOOL_THUMB_DOWN:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_DOWN, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed);
         break;
      //--- Create a left price label at the clicked position
      case TOOL_PRICE_LABEL:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_LEFT_PRICE, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue);
         break;
      //--- Create a stop sign annotation at the clicked position
      case TOOL_STOP_SIGN:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_STOP, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed);
         break;
      //--- Create a check mark annotation at the clicked position
      case TOOL_CHECK_MARK:
         ok = ObjectCreate(m_chartId, name, OBJ_ARROW_CHECK, sub, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime);
         break;
      //--- Create a Fibonacci time zones object at the clicked position
      case TOOL_FIBO_TIMEZONES:
         ok = ObjectCreate(m_chartId, name, OBJ_FIBOTIMES, sub, t, p, t, p);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrGold);
         break;
      default: break;
     }
   //--- Mark created objects as selectable and trigger a chart redraw
   if (ok)
     {
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true);
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true);
      ChartRedraw(m_chartId);
     }
  }

//+------------------------------------------------------------------+
//| Create a chart object that requires two placement clicks         |
//+------------------------------------------------------------------+
void CDrawingEngine::CreateTwoClickObject(int sub, TOOL_TYPE toolType)
  {
   string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue;
   //--- Retrieve the two recorded anchor points
   datetime t1 = m_drawPoint1Time,  t2 = m_drawPoint2Time;
   double   p1 = m_drawPoint1Price, p2 = m_drawPoint2Price;
   switch (toolType)
     {
      //--- Create a standard trend line between the two anchor points
      case TOOL_TRENDLINE:
         ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2);
         break;
      //--- Create a ray line starting at the first anchor and extending right
      case TOOL_RAY:
         ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true);
         break;
      //--- Create an extended line running through both anchors in both directions
      case TOOL_EXTENDED_LINE:
         ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2);
         if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_RAY_LEFT, true); ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true); }
         break;
      //--- Create a measure/info line and annotate it with pip distance
      case TOOL_INFO_LINE:
         ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2);
         if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, StringFormat("%.0f pips", MathAbs(p2 - p1) / SymbolInfoDouble(_Symbol, SYMBOL_POINT) / 10.0)); objColor = clrMediumSlateBlue; }
         break;
      //--- Create a filled rectangle between the two anchor points
      case TOOL_RECTANGLE:
         ok = ObjectCreate(m_chartId, name, OBJ_RECTANGLE, sub, t1, p1, t2, p2);
         if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_FILL, true);
         break;
      //--- Create a triangle with a computed third vertex
      case TOOL_TRIANGLE:
         ok = ObjectCreate(m_chartId, name, OBJ_TRIANGLE, sub, t1, p1, t2, p2, t1 + (t2 - t1) / 2, p1 - MathAbs(p2 - p1));
         if (ok) objColor = clrMediumSlateBlue;
         break;
      //--- Create an ellipse using anchor points and a mid-edge third point
      case TOOL_ELLIPSE:
         ok = ObjectCreate(m_chartId, name, OBJ_ELLIPSE, sub, t1, p1, t2, p2, t1, p1 + (p2 - p1) / 2);
         if (ok) objColor = clrMediumOrchid;
         break;
      //--- Create a Fibonacci retracement between the two anchor points
      case TOOL_FIBO_RETRACEMENT:
         ok = ObjectCreate(m_chartId, name, OBJ_FIBO, sub, t1, p1, t2, p2);
         if (ok) objColor = clrGold;
         break;
      //--- Create a Fibonacci expansion between the two anchor points
      case TOOL_FIBO_EXPANSION:
         ok = ObjectCreate(m_chartId, name, OBJ_EXPANSION, sub, t1, p1, t2, p2);
         if (ok) objColor = clrGold;
         break;
      //--- Create a Fibonacci fan between the two anchor points
      case TOOL_FIBO_FAN:
         ok = ObjectCreate(m_chartId, name, OBJ_FIBOFAN, sub, t1, p1, t2, p2);
         if (ok) objColor = clrGold;
         break;
      //--- Create Fibonacci arcs between the two anchor points
      case TOOL_FIBO_ARCS:
         ok = ObjectCreate(m_chartId, name, OBJ_FIBOARC, sub, t1, p1, t2, p2);
         if (ok) objColor = clrGold;
         break;
      //--- Create a Gann line between the two anchor points
      case TOOL_GANN_LINE:
         ok = ObjectCreate(m_chartId, name, OBJ_GANNLINE, sub, t1, p1, t2, p2);
         if (ok) objColor = clrOrangeRed;
         break;
      //--- Create a Gann fan from the first anchor point
      case TOOL_GANN_FAN:
         ok = ObjectCreate(m_chartId, name, OBJ_GANNFAN, sub, t1, p1, t2, p2);
         if (ok) objColor = clrOrangeRed;
         break;
      //--- Create a Gann grid between the two anchor points
      case TOOL_GANN_GRID:
         ok = ObjectCreate(m_chartId, name, OBJ_GANNGRID, sub, t1, p1, t2, p2);
         if (ok) objColor = clrOrangeRed;
         break;
      //--- Create a regression channel between the two anchor points
      case TOOL_REGRESSION_CHANNEL:
         ok = ObjectCreate(m_chartId, name, OBJ_REGRESSION, sub, t1, p1, t2, p2);
         if (ok) objColor = clrCornflowerBlue;
         break;
      //--- Create a standard deviation channel between the two anchor points
      case TOOL_STDDEV_CHANNEL:
         ok = ObjectCreate(m_chartId, name, OBJ_STDDEVCHANNEL, sub, t1, p1, t2, p2);
         if (ok) objColor = clrCornflowerBlue;
         break;
      default: break;
     }
   //--- Apply shared properties and trigger a chart redraw
   if (ok)
     {
      ObjectSetInteger(m_chartId, name, OBJPROP_COLOR,      objColor);
      ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH,      1);
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true);
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED,   true);
      ChartRedraw(m_chartId);
     }
  }

//+------------------------------------------------------------------+
//| Create a chart object that requires three placement clicks       |
//+------------------------------------------------------------------+
void CDrawingEngine::CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType)
  {
   string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue;
   //--- Retrieve the first two recorded anchor points
   datetime t1 = m_drawPoint1Time,  t2 = m_drawPoint2Time;
   double   p1 = m_drawPoint1Price, p2 = m_drawPoint2Price;
   switch (toolType)
     {
      //--- Create a parallel channel using all three anchor points
      case TOOL_PARALLEL_CHANNEL:
         ok = ObjectCreate(m_chartId, name, OBJ_CHANNEL, sub, t1, p1, t2, p2, t3, p3);
         if (ok) objColor = clrCornflowerBlue;
         break;
      //--- Create a Fibonacci channel using all three anchor points
      case TOOL_FIBO_CHANNEL:
         ok = ObjectCreate(m_chartId, name, OBJ_FIBOCHANNEL, sub, t1, p1, t2, p2, t3, p3);
         if (ok) objColor = clrGold;
         break;
      //--- Create a pitchfork variant using all three anchor points
      case TOOL_PITCHFORK:
      case TOOL_SCHIFF_PITCHFORK:
      case TOOL_MOD_SCHIFF:
         ok = ObjectCreate(m_chartId, name, OBJ_PITCHFORK, sub, t1, p1, t2, p2, t3, p3);
         if (ok) objColor = clrMediumSeaGreen;
         break;
      default: break;
     }
   //--- Apply shared properties and trigger a chart redraw
   if (ok)
     {
      ObjectSetInteger(m_chartId, name, OBJPROP_COLOR,      objColor);
      ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH,      1);
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true);
      ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED,   true);
      ChartRedraw(m_chartId);
     }
  }

Мы объявляем класс "CDrawingEngine", который наследует от "CChartEventHandler" и добавляет защищенные члены для счетчика имен объектов, текущего количества щелчков в последовательности размещения и двух пар переменных времени и цены, хранящих первую и вторую якорные точки. Мы объявляем пять защищенных методов: "MakeUniqueObjectName" для генерации уникальных имен объектов, "HandleDrawingClick" для обработки каждого щелчка по графику и три метода создания для инструментов с одним, двумя и тремя щелчками.

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

Метод "CreateSingleClickObject" обрабатывает двенадцать типов инструментов с помощью оператора switch. Горизонтальные и вертикальные линии создаются с помощью ObjectCreate с использованием OBJ_HLINE и OBJ_VLINE с пунктирным стилем. Текстовые метки, стрелочные аннотации, ползунки вверх и вниз, ценовые метки, знаки остановки, галочки и временные зоны Фибоначчи создают соответствующие типы объектов MQL5 с соответствующими цветами. Все созданные объекты помечаются как выбираемые и выбранные.

Метод "CreateTwoClickObject" обрабатывает шестнадцать типов инструментов, используя две сохраненные якорные точки. Линии тренда, лучи и продолженные линии используют OBJ_TREND, при этом свойства лучей переключаются соответствующим образом. Информационная линия добавляет аннотацию расстояния в пипсах. Прямоугольники создаются с включенной заливкой, треугольники автоматически вычисляют третью вершину, а эллипсы используют третью точку посередине ребра. Коррекции Фибоначчи, расширения, веера и дуги используют каждый свой выделенный тип объекта золотым цветом. Инструменты Ганна используют оранжево-красный цвет, а типы каналов — васильково-синий. Все объекты получают общий цвет, ширину, возможность выбора и выбранные свойства.

Метод "CreateThreeClickObject" обрабатывает параллельные каналы, каналы Фибоначчи и варианты вил, используя все три точки привязки, создавая соответствующий тип объекта MQL5 и применяя общие свойства перед перерисовкой графика. Наконец, мы все подключаем заново, так что теперь все управление осуществляется из одного центрального класса.

Перестройка оболочки боковой панели верхнего уровня

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

//+------------------------------------------------------------------+
//| CLASS 10 — Top-level sidebar shell exposing the public interface |
//+------------------------------------------------------------------+
class CToolsSidebar : public CDrawingEngine
  {
private:
   TOOL_TYPE m_currentActiveTool;   // Currently active drawing tool, or TOOL_NONE
   string    m_currentInstruction;  // Instruction text shown during multi-click tool placement

public:
   CToolsSidebar()  { InitDefaults(); } // Construct and apply default state
   ~CToolsSidebar() { Destroy(); }      // Destruct and clean up all resources

   //--- Initialize the sidebar and build all canvas objects
   bool Init(long chartId);
   //--- Destroy all canvas objects and release resources
   void Destroy();
   //--- Handle all incoming chart events for the sidebar
   void OnEvent(const int id, const long &lp, const double &dp, const string &sp);

private:
   //--- Set all member variables to their compile-time default values
   void InitDefaults();
   //--- Toggle the given tool on or off as the active drawing tool
   void ToggleTool(TOOL_TYPE toolType);
   //--- Deactivate the current tool and reset placement state
   void DeactivateCurrentTool();
   //--- Remove all drawing objects placed by this indicator instance
   void CleanupAllDrawnObjects();
  };

//+------------------------------------------------------------------+
//| Set all member variables to their compile-time default values    |
//+------------------------------------------------------------------+
void CToolsSidebar::InitDefaults()
  {
   //--- Reset chart reference to default
   m_chartId               = 0;
   //--- Set the sidebar bitmap label object name
   m_nameSidebar           = "ToolsPalette_Sidebar";
   //--- Set the flyout bitmap label object name
   m_nameFlyout            = "ToolsPalette_Flyout";
   //--- Set supersampling factor for high-res rendering
   m_supersampleFactor     = 4;
   //--- Set default button size and spacing
   m_categoryButtonSize    = 36;
   m_categoryButtonPadding = 6;
   //--- Set panel corner rounding radius
   m_panelCornerRadius     = 10;
   //--- Set header and grip strip height
   m_headerGripHeight      = 92;
   //--- Set sidebar panel width
   m_sidebarWidth          = 48;
   //--- Reset computed height and visible category count
   m_sidebarHeight         = 0;
   m_sidebarMaxVisibleCats = 0;
   //--- Reset scroll state
   m_sidebarScrollPixels         = 0;
   m_sidebarScrollThumbHeight    = 30;
   m_sidebarScrollThinWidth      = 3;
   m_isSidebarThumbDragging      = false;
   m_sidebarThumbDragStartY      = 0;
   m_sidebarThumbDragStartPixels = 0;
   m_isHoveredSidebarScrollArea  = false;
   m_isHoveredSidebarThumb       = false;
   //--- Reset panel position to origin
   m_panelX = 0;
   m_panelY = CanvasY;
   //--- Default snap state to left edge
   m_snapState       = SNAP_LEFT;
   //--- Reset panel drag state
   m_isPanelDragging = false;
   m_dragOffsetX     = 0;
   m_dragOffsetY     = 0;
   //--- Reset snapped height override and resize state
   m_snappedSidebarHeight      = 0;
   m_isResizingBottomEdge      = false;
   m_bottomResizeDragStartY    = 0;
   m_bottomResizeStartHeight   = 0;
   m_isBottomResizeHovered     = false;
   //--- Reset all header button hover flags
   m_hoveredCategory         = CAT_NONE;
   m_isCloseButtonHovered    = false;
   m_isThemeButtonHovered    = false;
   m_isGripAreaHovered       = false;
   //--- Set flyout layout defaults
   m_flyoutWidth             = 195;
   m_flyoutItemHeight        = 32;
   m_flyoutPadding           = 7;
   m_flyoutPointerWidth      = 10;
   m_flyoutPointerHeight     = 8;
   m_flyoutPointerLocalY     = 40;
   m_flyoutPointerOnLeft     = true;
   //--- Reset flyout visibility and category state
   m_isFlyoutVisible         = false;
   m_flyoutActiveCat         = CAT_NONE;
   m_hoveredFlyoutItem       = -1;
   //--- Reset flyout scroll state
   m_flyoutScrollPixels         = 0;
   m_flyoutMaxVisibleItems      = 5;
   m_flyoutScrollThumbHeight    = 30;
   m_isFlyoutThumbDragging      = false;
   m_flyoutThumbDragStartY      = 0;
   m_flyoutThumbDragStartPixels = 0;
   m_isHoveredFlyoutScrollArea  = false;
   m_isHoveredFlyoutThumb       = false;
   //--- Apply the starting theme from user input
   m_isDarkTheme              = StartDark;
   //--- Reset mouse button tracking
   m_previousMouseButtonState = 0;
   //--- Reset active tool and instruction text
   m_currentActiveTool   = TOOL_NONE;
   m_currentInstruction  = "";
   //--- Reset drawing object placement state
   m_drawnObjectCounter    = 0;
   m_toolDrawingClickCount = 0;
   m_drawPoint1Time        = 0;
   m_drawPoint2Time        = 0;
   m_drawPoint1Price       = 0.0;
   m_drawPoint2Price       = 0.0;
  }

//+------------------------------------------------------------------+
//| Initialize the sidebar and build all canvas objects              |
//+------------------------------------------------------------------+
bool CToolsSidebar::Init(long chartId)
  {
   //--- Reset all members to defaults before initializing
   InitDefaults();
   //--- Store the target chart identifier
   m_chartId = chartId;
   //--- Set initial panel X position based on default snap state
   m_panelX = (m_snapState == SNAP_RIGHT) ? (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - m_sidebarWidth : 0;
   //--- Populate all category and tool definitions
   InitAllCategoriesAndTools();
   //--- Apply the active theme color set
   ApplyTheme();
   //--- Compute and set the sidebar panel height
   CalcSidebarHeight();
   //--- Create all canvas layers; abort on failure
   if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false;
   //--- Position the sidebar chart object
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100);
   //--- Set the flyout Z-order above the sidebar
   ObjectSetInteger(0, m_nameFlyout,  OBJPROP_ZORDER, 200);
   //--- Hide the flyout and draw the initial sidebar frame
   HideFlyout();
   DrawSidebar(m_currentActiveTool);
   //--- Reapply position in case the draw call shifted the object
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   //--- Enable mouse move and wheel events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   ChartSetInteger(0, CHART_MOUSE_SCROLL,      true);
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and release resources                 |
//+------------------------------------------------------------------+
void CToolsSidebar::Destroy()
  {
   //--- Deactivate the active tool before destroying
   m_currentActiveTool = TOOL_NONE;
   //--- Delegate canvas cleanup to the layer base class
   DestroyAllCanvases();
   //--- Remove all chart drawing objects placed by this instance
   CleanupAllDrawnObjects();
   //--- Restore chart mouse scroll to the default state
   ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
  }

//+------------------------------------------------------------------+
//| Toggle the given tool on or off as the active drawing tool       |
//+------------------------------------------------------------------+
void CToolsSidebar::ToggleTool(TOOL_TYPE toolType)
  {
   //--- Deactivate if the pointer tool is selected or the tool is already active
   if (toolType == TOOL_POINTER || m_currentActiveTool == toolType)
     {
      m_currentActiveTool     = TOOL_NONE;
      m_toolDrawingClickCount = 0;
      m_currentInstruction    = "";
     }
   else
     {
      //--- Activate the new tool and reset its click counter and instruction
      m_currentActiveTool     = toolType;
      m_toolDrawingClickCount = 0;
      m_currentInstruction    = "Click on chart to place " + GetToolLabel(toolType) + ".";
     }
  }

//+------------------------------------------------------------------+
//| Deactivate the current tool and reset placement state            |
//+------------------------------------------------------------------+
void CToolsSidebar::DeactivateCurrentTool()
  {
   //--- Clear tool state and instruction text
   m_currentActiveTool     = TOOL_NONE;
   m_toolDrawingClickCount = 0;
   m_currentInstruction    = "";
   //--- Redraw the sidebar to reflect the deactivated state
   DrawSidebar(m_currentActiveTool);
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Remove all drawing objects placed by this indicator instance     |
//+------------------------------------------------------------------+
void CToolsSidebar::CleanupAllDrawnObjects()
  {
   //--- Iterate backwards through all chart objects to safely delete matches
   int total = ObjectsTotal(m_chartId);
   for (int i = total - 1; i >= 0; i--)
     {
      string n = ObjectName(m_chartId, i);
      //--- Delete objects whose names match the drawing prefix
      if (StringFind(n, "ToolsPalette_Drawing_") == 0) ObjectDelete(m_chartId, n);
     }
  }

//+------------------------------------------------------------------+
//| Handle all incoming chart events for the sidebar                 |
//+------------------------------------------------------------------+
void CToolsSidebar::OnEvent(const int id, const long &lp, const double &dp, const string &sp)
  {
   //--- Deactivate the active tool when Escape is pressed
   if (id == CHARTEVENT_KEYDOWN && lp == 27) { DeactivateCurrentTool(); return; }

   if (id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX = (int)lp, mouseY = (int)dp, mouseButtons = (int)sp;
      int lx, ly, flx, fly;
      bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly);
      bool overFlyout  = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly);

      //--- Handle theme toggle on fresh left-click over the theme button
      if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_isThemeButtonHovered)
        {
         ToggleTheme();
         DrawSidebar(m_currentActiveTool);
         if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, m_currentActiveTool);
         ChartRedraw();
         m_previousMouseButtonState = mouseButtons;
         return;
        }

      //--- Handle tool selection from flyout on fresh left-click over a flyout item
      if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE)
        {
         int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
         if (m_hoveredFlyoutItem < nT)
           {
            //--- Toggle the selected tool and redraw
            ToggleTool(m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].toolType);
            HideFlyout();
            DrawSidebar(m_currentActiveTool);
            ChartRedraw();
           }
         m_previousMouseButtonState = mouseButtons;
         return;
        }

      //--- Handle direct tool selection for single-tool categories on fresh left-click
      if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_hoveredCategory != CAT_NONE &&
          !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered &&
          ArraySize(m_categories[(int)m_hoveredCategory].tools) == 1)
        {
         ToggleTool(m_categories[(int)m_hoveredCategory].tools[0].toolType);
         HideFlyout();
         DrawSidebar(m_currentActiveTool);
         ChartRedraw();
         m_previousMouseButtonState = mouseButtons;
         return;
        }

      //--- Handle chart placement clicks when a drawing tool is active
      if (mouseButtons == 1 && m_previousMouseButtonState == 0 &&
          m_currentActiveTool != TOOL_NONE && m_currentActiveTool != TOOL_POINTER &&
          !overSidebar && !overFlyout)
        {
         HandleDrawingClick(mouseX, mouseY, m_currentActiveTool, m_currentInstruction);
         DrawSidebar(m_currentActiveTool);
         m_previousMouseButtonState = mouseButtons;
         return;
        }

      //--- Forward remaining mouse move logic to the chart event handler
      RouteChartEvent(id, lp, dp, sp, m_currentActiveTool);

      //--- Update the sidebar tooltip based on current hover state
      string tip = "";
      if (overSidebar && m_hoveredCategory != CAT_NONE)
         tip = m_categories[(int)m_hoveredCategory].categoryLabel;
      if (overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE)
        {
         int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools);
         if (m_hoveredFlyoutItem < nT) tip = m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].tooltipText;
        }
      ObjectSetString(0, m_nameSidebar, OBJPROP_TOOLTIP, tip);
      return;
     }

   //--- Forward all other chart events to the routing handler
   RouteChartEvent(id, lp, dp, sp, m_currentActiveTool);
  }

Во-первых, мы объявляем класс "CToolsSidebar", который теперь наследует от "CDrawingEngine" вместо "CSidebarRenderer", как в предыдущей части, что дает ему доступ ко всей иерархии из десяти классов. Мы добавляем два закрытых члена: текущий активный тип инструмента и строку инструкций, отображаемую при размещении несколькими щелчками мыши. Открытый интерфейс остается прежним с "Init", "Destroy" и «OnEvent", в то время как четыре приватных метода обрабатывают значения по умолчанию, переключение инструментов, деактивацию инструментов и очистку объектов рисования.

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

Метод "Init" управляет всей последовательностью запуска. Мы вызываем "InitDefaults", сохраняем идентификатор диаграммы, вычисляем начальное положение панели на основе состояния привязки, вызываем "InitAllCategoriesAndTools" для заполнения полного реестра инструментов, применяем тему, вычисляем высоту боковой панели, создаем все четыре холста, позиционируем объекты графика боковой панели и задаем для выдвижной панели более высокий Z-порядок, скрываем выдвижную панель, рисуем начальную рамку боковой панели и включаем события перемещения мыши и колеса мыши с помощью функции ChartSetInteger

Метод Destroy деактивирует текущий инструмент, делегирует очистку холста методу "DestroyAllCanvases", вызывает метод "CleanupAllDrawnObjects" для удаления всех размещенных объектов графика путем обратного перебора ObjectsTotal и удаления имен, соответствующих префиксу рисования, с помощью StringFind, и восстанавливает прокрутку графика.

Метод "ToggleTool" деактивирует инструмент, если указатель выбран или тот же инструмент нажат снова, очищая счетчик кликов и инструкцию. В противном случае он активирует новый инструмент, сбрасывает счетчик и устанавливает текст инструкции с помощью метода "GetToolLabel". Метод "DeactivateCurrentTool" очищает состояние инструмента, перерисовывает боковую панель и вызывается при нажатии клавиши Escape.

Метод "OnEvent" является основной точкой входа для всех событий графика. Он обрабатывает нажатия клавиши Escape для деактивации инструмента, а затем обрабатывает события перемещения мыши в цепочке приоритетов: щелчки по переключателю темы вызывают функцию "ToggleTheme" и перерисовывают обе панели, щелчки по элементам выдвижной панели вызывают функцию "ToggleTool" с выбранным инструментом и скрывают выдвижную панель, щелчки по категориям отдельных инструментов активируют инструмент напрямую, а щелчки по размещению графика, когда активен инструмент рисования, вызывают функцию "HandleDrawingClick". Вся оставшаяся логика работы с мышью передается в функцию "RouteChartEvent". Мы также обновляем всплывающую подсказку боковой панели в зависимости от состояния при наведении курсора, используя функцию ObjectSetString. Все остальные типы событий перенаправляются непосредственно обработчику маршрутизации. Таким образом, теперь вся иерархия управления соединена между собой, что и позволяет достичь нашей цели. Далее мы протестируем программу в следующем разделе.


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

Мы скомпилировали программу и прикрепили ее к графику. Ниже показана итоговая визуализация в виде GIF-изображения.

PALETTE BACKTEST GIF

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


Заключение

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

Критерии приемки, продемонстрированные в тестировании, ясны: выбор инструмента из выдвижной панели приводит к размещению объекта на графике; переполнение списков происходит при прокрутке колесом мыши или перетаскивании ползунком. Панель перетаскивается и привязывается к краям графика. Переключатели тем применяются немедленно, а выдвижные панели корректно перемещаются при перемещении панели. Архитектура обеспечивает четкий контракт взаимодействия — активный инструмент, необходимые щелчки и состояние размещения — что делает поведение предсказуемым и упрощает его расширение. После прочтения статьи вы сможете:

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (4)
Mustafa Nail Sertoglu
Mustafa Nail Sertoglu | 16 мая 2026 в 14:46
Выглядит ЗДОРОВО; спасибо, что поделились кодами и идеями; берегите себя
Allan Munene Mutiiria
Allan Munene Mutiiria | 17 мая 2026 в 10:08
Mustafa Nail Sertoglu #:
Выглядит ЗДОРОВО; спасибо, что поделились кодами и идеями; берегите себя
Конечно. Спасибо и вам, и добро пожаловать.
Denis Kislicyn
Denis Kislicyn | 12 июн. 2026 в 16:30
Какой прекрасный вид. Спасибо.
Allan Munene Mutiiria
Allan Munene Mutiiria | 12 июн. 2026 в 17:19
Denis Kislicyn #:
Какой прекрасный вид. Спасибо.
Спасибо за добрые отзывы. Не за что.
Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Разработка инструментария для анализа Price Action (Часть 68): Панель RSI с привязкой к цене на языке MQL5 Разработка инструментария для анализа Price Action (Часть 68): Панель RSI с привязкой к цене на языке MQL5
Мы представляем встроенную в график панель RSI, которая устраняет необходимость в отдельном окне, привязывая данные о моментуме непосредственно к текущей цене. В статье рассматриваются концепция решения и код на MQL5: получение значений RSI в реальном времени, классификация сигналов по наклону и адаптивное позиционирование. Трейдеры получают значение RSI, состояние и силу сигнала прямо в точке принятия решения, что повышает ясность анализа на разных таймфреймах.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Рыночные секреты Ларри Уильямса (Часть 14): Обнаружение разворотов Hidden Smash Day с помощью пользовательского индикатора Рыночные секреты Ларри Уильямса (Часть 14): Обнаружение разворотов Hidden Smash Day с помощью пользовательского индикатора
В этой статье разрабатывается практический индикатор MQL5, который обнаруживает бары Hidden Smash Day по строгим числовым критериям и, при необходимости, по подтверждению на следующей сессии. Рассматриваются процедуры обнаружения, регистрация буферов и настройка отрисовки, позволяющая размещать стрелки на барах, соответствующих условиям. Такой подход дает стабильные, не перерисовывающиеся сигналы для исторического тестирования и мониторинга в реальном времени.