English
preview
Торговые инструменты на MQL5 (Часть 30): Боковая панель палитры инструментов с архитектурой на основе классов

Торговые инструменты на MQL5 (Часть 30): Боковая панель палитры инструментов с архитектурой на основе классов

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

Введение

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

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

В предыдущей статье (Часть 29) мы анимировали бабочку на графике с помощью последовательного рисования, затухания крыльев и движения в полёте. В Части 30 мы возвращаемся к палитре инструментов, которую первоначально создали в Части 19, и перестраиваем ее с нуля, используя многослойную объектно-ориентированную архитектуру боковой панели, рендеринг на canvas с суперсэмплированием, управление темами и реестр категорий. Мы рассмотрим следующие темы:

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

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


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

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

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

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

SNAP TOOLS PALETTE ARCHITECTURE


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

Включение библиотеки Canvas, определение значков, перечислений, входных параметров и структур

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

//+------------------------------------------------------------------+
//|                                         Tools Palette Part 2.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

//--- Include canvas drawing library
#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Icon definitions — category sidebar buttons only                 |
//+------------------------------------------------------------------+
struct SIconDefinition { string fontName; uchar charCode; }; // Store icon font and character code

//--- Define icon for each tool category using font/char pairs
SIconDefinition ICON_CATEGORY_CURSORS     = { "Wingdings",   (uchar)'v' }; // Cursors category icon
SIconDefinition ICON_CATEGORY_LINES       = { "Wingdings 3", (uchar)'&' }; // Lines category icon
SIconDefinition ICON_CATEGORY_CHANNELS    = { "Wingdings 3", (uchar)'2' }; // Channels category icon
SIconDefinition ICON_CATEGORY_PITCHFORK   = { "Wingdings 3", (uchar)'H' }; // Pitchfork category icon
SIconDefinition ICON_CATEGORY_GANN        = { "Wingdings",   (uchar)'T' }; // Gann category icon
SIconDefinition ICON_CATEGORY_FIBONACCI   = { "Wingdings",   (uchar)'z' }; // Fibonacci category icon
SIconDefinition ICON_CATEGORY_SHAPES      = { "Wingdings",   (uchar)'o' }; // Shapes category icon
SIconDefinition ICON_CATEGORY_ANNOTATIONS = { "Webdings",    (uchar)'>' }; // Annotations category icon

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ENUM_SNAP_STATE
  {
   SNAP_LEFT,  // Snap sidebar to left edge
   SNAP_RIGHT, // Snap sidebar to right edge
   SNAP_FLOAT  // Allow sidebar to float freely
  };

enum ENUM_CATEGORY
  {
   CAT_CURSORS     = 0, // Cursors category index
   CAT_LINES,           // Lines category index
   CAT_CHANNELS,        // Channels category index
   CAT_PITCHFORK,       // Pitchfork category index
   CAT_GANN,            // Gann category index
   CAT_FIBONACCI,       // Fibonacci category index
   CAT_SHAPES,          // Shapes category index
   CAT_ANNOTATIONS,     // Annotations category index
   CAT_COUNT            // Total number of categories
  };

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input int    CanvasY           = 50;   // Canvas Y Position
input double BackgroundOpacity = 0.92; // Background Opacity (0.0 - 1.0)
input bool   StartDark         = true; // Start In Dark Theme
input int    BorderWidth       = 1;    // Border Width (px)
input int    CategoryIconSize  = 26;   // Category Icon Size (pt)
input int    SnapThreshold     = 40;   // Edge Snap Threshold (px)

//+------------------------------------------------------------------+
//| Category definition structure                                    |
//+------------------------------------------------------------------+
struct CategoryDefinition
  {
   string categoryLabel;    // Display label for the category
   string iconFontName;     // Font name used to render the icon
   uchar  iconCharCode;     // Character code of the icon glyph
   bool   hasMultipleTools; // Flag indicating category has sub-tools
  };

//+------------------------------------------------------------------+
//| 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 buttonIconColor;   // Color used to render category icons
   color gripDotsColor;     // Color of the drag-grip dot indicators
   color separatorColor;    // Color of the horizontal separator lines
  };

Мы начинаем с включения заголовочного файла "Canvas/Canvas.mqh", который предоставляет класс CCanvas, используемый во всей программе для рисования растровых изображений, рендеринга текста и операций с пикселями на объектах графика. Далее мы определяем структуру "SIconDefinition" для связывания имени шрифта с кодом символа, чтобы каждой категории инструментов можно было назначить визуальный значок. Далее объявляем восемь глобальных экземпляров этой структуры, по одному для каждой категории, такой как курсоры, линии, каналы, вилы, метод Ганна, числа Фибоначчи, фигуры и аннотации, каждый из которых сопоставляется с определенным глифом из семейств шрифтов Wingdings, Wingdings 3 или Webdings. Вы можете использовать любой шрифт или символы по своему усмотрению. Ниже показан используемый нами список.

SYMBOL FONTS

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

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

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

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

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

//+------------------------------------------------------------------+
//| CLASS 1 — Blend and draw low-level canvas primitives             |
//+------------------------------------------------------------------+
class CCanvasPrimitives
  {
protected:
   //--- Blend a single pixel onto the canvas using alpha compositing
   void BlendPixelSet(CCanvas &canvas, int x, int y, uint sourceARGB);
   //--- Downsample a high-res canvas into a lower-res destination
   void DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor);
   //--- Fill a single corner quadrant of a rounded rectangle at high res
   void FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY);
   //--- Fill a full rounded rectangle at high resolution
   void FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb);
   //--- Fill a rounded rectangle with per-corner rounding control at high res
   void FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
                                  bool rTL, bool rTR, bool rBL, bool rBR);
   //--- Fill a quadrilateral shape using scanline rasterization
   void FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb);
   //--- Draw a single thick border edge between two points
   void DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb);
   //--- Check whether an angle falls within a given arc range
   bool IsAngleBetween(double angle, double startAngle, double endAngle);
   //--- Draw a corner arc segment with specified thickness and angle range
   void DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle);
   //--- Draw a rounded rectangle border with per-corner rounding control at high res
   void DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
                                        bool rTL, bool rTR, bool rBL, bool rBR);
  };

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

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

Для отрисовки границ метод "FillQuadrilateralBorder" растеризует произвольную четырехстороннюю фигуру с помощью заливки сканирующими линиями, а метод "DrawBorderEdge" использует ее для рисования толстого отрезка между двумя точками путем построения перпендикулярного четырехугольника. Метод "IsAngleBetween" — это утилита, которая проверяет, попадает ли угол в диапазон дуг, поддерживая метод "DrawCornerArc", который отрисовывает сглаженные сегменты дуги заданной толщины для скругленных углов границ. Наконец, метод "DrawSelectiveRoundRectBorderHR" объединяет эти методы для рисования полного контура закругленного прямоугольника с возможностью управления закруглением каждого угла отдельно. Вместе эти примитивы образуют основу для отрисовки, на которой строится каждый визуальный элемент в боковой панели. Теперь мы определим эти методы, используя оператор разрешения области видимости. Для доступа к определенным элементам мы используем двойные двоеточия (::). Вот иллюстративный пример:

SCOPE RESOLUTION OPERATOR ILLUSTRATION

Реализация методов примитивов canvas

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

//+------------------------------------------------------------------+
//| Blend source pixel onto canvas using alpha compositing           |
//+------------------------------------------------------------------+
void CCanvasPrimitives::BlendPixelSet(CCanvas &canvas, int x, int y, uint src)
  {
   //--- Skip pixels outside canvas bounds
   if (x < 0 || x >= canvas.Width() || y < 0 || y >= canvas.Height()) return;
   //--- Read existing destination pixel
   uint dst = canvas.PixelGet(x, y);
   //--- Unpack source ARGB channels to normalized floats
   double sA = ((src >> 24) & 0xFF) / 255.0, sR = ((src >> 16) & 0xFF) / 255.0;
   double sG = ((src >>  8) & 0xFF) / 255.0, sB = ( src        & 0xFF) / 255.0;
   //--- Unpack destination ARGB channels to normalized floats
   double dA = ((dst >> 24) & 0xFF) / 255.0, dR = ((dst >> 16) & 0xFF) / 255.0;
   double dG = ((dst >>  8) & 0xFF) / 255.0, dB = ( dst        & 0xFF) / 255.0;
   //--- Compute output alpha using standard over-compositing formula
   double oA = sA + dA * (1.0 - sA);
   //--- Write fully transparent pixel and exit if output alpha is zero
   if (oA == 0.0) { canvas.PixelSet(x, y, 0); return; }
   //--- Write blended ARGB pixel to canvas
   canvas.PixelSet(x, y,
      ((uint)(uchar)(oA * 255 + 0.5) << 24) |
      ((uint)(uchar)((sR * sA + dR * dA * (1.0 - sA)) / oA * 255 + 0.5) << 16) |
      ((uint)(uchar)((sG * sA + dG * dA * (1.0 - sA)) / oA * 255 + 0.5) <<  8) |
       (uint)(uchar)((sB * sA + dB * dA * (1.0 - sA)) / oA * 255 + 0.5));
  }

//+------------------------------------------------------------------+
//| Downsample high-res canvas into destination by averaging pixels  |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DownsampleCanvas(CCanvas &dst, CCanvas &src, int factor)
  {
   //--- Cache destination dimensions and squared sample count
   int dW = dst.Width(), dH = dst.Height(), ss2 = factor * factor;
   //--- Iterate over every destination pixel
   for (int py = 0; py < dH; py++)
      for (int px = 0; px < dW; px++)
        {
         //--- Accumulate channel sums across the source sample block
         double sA = 0, sR = 0, sG = 0, sB = 0, wc = 0;
         for (int dy = 0; dy < factor; dy++)
            for (int dx = 0; dx < factor; dx++)
              {
               //--- Compute source sample coordinates
               int sx = px * factor + dx, sy = py * factor + dy;
               //--- Skip samples outside source bounds
               if (sx >= src.Width() || sy >= src.Height()) continue;
               //--- Read source pixel and extract alpha
               uint p = src.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF);
               //--- Accumulate alpha unconditionally
               sA += a;
               //--- Accumulate color channels only for non-transparent samples
               if (a > 0) { sR += (p >> 16) & 0xFF; sG += (p >> 8) & 0xFF; sB += p & 0xFF; wc += 1.0; }
              }
         //--- Compute averaged output alpha
         uchar fa = (uchar)(sA / ss2);
         //--- Write transparent pixel and skip if result is fully transparent
         if (fa == 0 || wc == 0) { dst.PixelSet(px, py, 0); continue; }
         //--- Write averaged ARGB pixel to destination canvas
         dst.PixelSet(px, py, ((uint)fa << 24) | ((uint)(uchar)(sR / wc) << 16) |
                               ((uint)(uchar)(sG / wc) << 8) | (uint)(uchar)(sB / wc));
        }
  }

//+------------------------------------------------------------------+
//| Fill one corner quadrant of a rounded rect at high resolution    |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillCornerQuadrantHR(CCanvas &canvas, int cx, int cy, int radius, uint argb, int signX, int signY)
  {
   //--- Cache radius as double and extract alpha and RGB components
   double rd = (double)radius;
   uchar  bA = (uchar)((argb >> 24) & 0xFF);
   uint   rgb = argb & 0x00FFFFFF;
   //--- Set sub-pixel sample count and derived values
   int sub = 4; double step = 1.0 / sub; int subSq = sub * sub;
   //--- Iterate over pixel neighbourhood around corner center
   for (int dy = -(radius + 1); dy <= (radius + 1); dy++)
      for (int dx = -(radius + 1); dx <= (radius + 1); dx++)
        {
         //--- Check pixel belongs to the target quadrant
         bool inQ = ((signX > 0) ? (dx >= 0) : (dx <= 0)) && ((signY > 0) ? (dy >= 0) : (dy <= 0));
         if (!inQ) continue;
         //--- Compute distance from corner center
         double dist = MathSqrt((double)(dx * dx + dy * dy));
         //--- Skip pixels too far outside radius
         if (dist > rd + 1.0) continue;
         //--- Fill pixels fully inside radius without anti-aliasing
         if (dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
         //--- Count sub-pixel samples falling inside the circle
         int inside = 0;
         for (int sy = 0; sy < sub; sy++)
            for (int sx = 0; sx < sub; sx++)
              {
               //--- Compute sub-pixel offsets from pixel center
               double sdx = (double)dx - 0.5 + (sx + 0.5) * step;
               double sdy = (double)dy - 0.5 + (sy + 0.5) * step;
               if (sdx * sdx + sdy * sdy <= rd * rd) inside++;
              }
         //--- Skip pixel if no sub-samples are inside
         if (inside == 0) continue;
         //--- Blend anti-aliased pixel using coverage fraction
         BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
        }
  }

//+------------------------------------------------------------------+
//| Fill a fully rounded rectangle at high resolution                |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb)
  {
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Fall back to plain rectangle when radius is zero or negative
   if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
   //--- Fill horizontal center strip
   canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
   //--- Fill left vertical strip
   canvas.FillRectangle(x, y + radius, x + radius - 1, y + h - radius - 1, argb);
   //--- Fill right vertical strip
   canvas.FillRectangle(x + w - radius, y + radius, x + w - 1, y + h - radius - 1, argb);
   //--- Fill top-left corner quadrant
   FillCornerQuadrantHR(canvas, x + radius,     y + radius,     radius, argb, -1, -1);
   //--- Fill top-right corner quadrant
   FillCornerQuadrantHR(canvas, x + w - radius, y + radius,     radius, argb,  1, -1);
   //--- Fill bottom-left corner quadrant
   FillCornerQuadrantHR(canvas, x + radius,     y + h - radius, radius, argb, -1,  1);
   //--- Fill bottom-right corner quadrant
   FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb,  1,  1);
  }

//+------------------------------------------------------------------+
//| Fill rounded rectangle with per-corner rounding at high res      |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillSelectiveRoundRectHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb,
                                                  bool rTL, bool rTR, bool rBL, bool rBR)
  {
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Fall back to plain rectangle when radius is zero or negative
   if (radius <= 0) { canvas.FillRectangle(x, y, x + w - 1, y + h - 1, argb); return; }
   //--- Fill horizontal center strip spanning full width
   canvas.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb);
   //--- Fill left strip, respecting top-left and bottom-left rounding
   canvas.FillRectangle(x,          y + (rTL ? radius : 0), x + radius - 1,  y + h - 1 - (rBL ? radius : 0), argb);
   //--- Fill right strip, respecting top-right and bottom-right rounding
   canvas.FillRectangle(x + w - radius, y + (rTR ? radius : 0), x + w - 1,   y + h - 1 - (rBR ? radius : 0), argb);
   //--- Fill or square top-left corner based on rounding flag
   if (rTL) FillCornerQuadrantHR(canvas, x + radius,     y + radius,     radius, argb, -1, -1);
   else     canvas.FillRectangle(x, y, x + radius - 1, y + radius - 1, argb);
   //--- Fill or square top-right corner based on rounding flag
   if (rTR) FillCornerQuadrantHR(canvas, x + w - radius, y + radius,     radius, argb,  1, -1);
   else     canvas.FillRectangle(x + w - radius, y, x + w - 1, y + radius - 1, argb);
   //--- Fill or square bottom-left corner based on rounding flag
   if (rBL) FillCornerQuadrantHR(canvas, x + radius,     y + h - radius, radius, argb, -1,  1);
   else     canvas.FillRectangle(x, y + h - radius, x + radius - 1, y + h - 1, argb);
   //--- Fill or square bottom-right corner based on rounding flag
   if (rBR) FillCornerQuadrantHR(canvas, x + w - radius, y + h - radius, radius, argb,  1,  1);
   else     canvas.FillRectangle(x + w - radius, y + h - radius, x + w - 1, y + h - 1, argb);
  }

//+------------------------------------------------------------------+
//| Fill a quadrilateral using scanline rasterization                |
//+------------------------------------------------------------------+
void CCanvasPrimitives::FillQuadrilateralBorder(CCanvas &canvas, double &vx[], double &vy[], uint argb)
  {
   //--- Find vertical bounding extent of the quad
   double minY = vy[0], maxY = vy[0];
   for (int i = 1; i < 4; 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)MathCeil(maxY) - 1; scanY++)
     {
      //--- Compute scanline center Y and prepare intersection buffer
      double cy = (double)scanY + 0.5; double xi[8]; int nc = 0;
      //--- Compute X intersections with each edge of the quad
      for (int i = 0; i < 4; i++)
        {
         int ni = (i + 1) % 4;
         //--- 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)MathCeil(xi[p + 1]) - 1; fx++)
            canvas.PixelSet(fx, scanY, argb);
     }
  }

//+------------------------------------------------------------------+
//| Draw a thick border edge between two endpoints                   |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawBorderEdge(CCanvas &canvas, double x0, double y0, double x1, double y1, int thickness, uint argb)
  {
   //--- Compute edge direction vector and length
   double dx = x1 - x0, dy = y1 - y0, len = MathSqrt(dx * dx + dy * dy);
   //--- Skip degenerate edges with near-zero length
   if (len < 1e-6) return;
   //--- Compute perpendicular and unit direction vectors
   double px = -dy / len, py = dx / len, ex = dx / len, ey = dy / len;
   //--- Compute half-thickness and end cap extension
   double ht = thickness / 2.0, ext = 0.23 * thickness;
   //--- Extend start and end points slightly for mitre cap effect
   double sx = x0 - ex * ext, sy = y0 - ey * ext, ex2 = x1 + ex * ext, ey2 = y1 + ey * ext;
   //--- Build quad vertices offset perpendicular to edge direction
   double tvx[4] = { sx - px*ht, sx + px*ht, ex2 + px*ht, ex2 - px*ht };
   double tvy[4] = { sy - py*ht, sy + py*ht, ey2 + py*ht, ey2 - py*ht };
   //--- Fill the resulting quad as the border edge shape
   FillQuadrilateralBorder(canvas, tvx, tvy, argb);
  }

//+------------------------------------------------------------------+
//| Check whether an angle falls within a start-to-end arc range     |
//+------------------------------------------------------------------+
bool CCanvasPrimitives::IsAngleBetween(double angle, double start, double end)
  {
   //--- Normalize all angles to the [0, 2π) range
   double tp = 2.0 * M_PI;
   angle = MathMod(angle + tp, tp); start = MathMod(start + tp, tp); end = MathMod(end + tp, tp);
   //--- Return true if angle lies within the arc from start to end
   return MathMod(angle - start + tp, tp) <= MathMod(end - start + tp, tp);
  }

//+------------------------------------------------------------------+
//| Draw an anti-aliased corner arc with specified thickness         |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawCornerArc(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb, double startAngle, double endAngle)
  {
   //--- Compute outer and inner radii for the arc ring
   double oR = (double)radius, iR = MathMax(0.0, (double)radius - thickness);
   //--- Extract alpha and RGB components from packed color
   uchar  bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF;
   //--- Set sub-pixel sample count and pixel scan radius
   int sub = 4; double step = 1.0 / sub; int subSq = sub * sub, pr = (int)(oR + 2.0);
   //--- Iterate over pixels in the bounding box of the arc
   for (int dy = -pr; dy <= pr; dy++)
      for (int dx = -pr; dx <= pr; dx++)
        {
         //--- Compute distance from arc center
         double dist = MathSqrt((double)(dx * dx + dy * dy));
         //--- Skip pixels clearly outside the ring or wrong angle
         if (dist > oR + 1.0 || dist < iR - 1.0) continue;
         if (!IsAngleBetween(MathArctan2((double)dy, (double)dx), startAngle, endAngle)) continue;
         //--- Fill pixels fully inside the ring without anti-aliasing
         if (dist <= oR - 1.0 && dist >= iR + 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; }
         //--- Count sub-pixel samples inside the arc ring and angle range
         int inside = 0;
         for (int sy = 0; sy < sub; sy++)
            for (int sx = 0; sx < sub; sx++)
              {
               double sdx = (double)dx - 0.5 + (sx + 0.5) * step, sdy = (double)dy - 0.5 + (sy + 0.5) * step;
               double sd = MathSqrt(sdx * sdx + sdy * sdy);
               if (sd >= iR && sd <= oR && IsAngleBetween(MathArctan2(sdy, sdx), startAngle, endAngle)) inside++;
              }
         //--- Skip pixel if no sub-samples qualify
         if (inside == 0) continue;
         //--- Write fully opaque pixel if all sub-samples qualify
         if (inside >= subSq) canvas.PixelSet(cx + dx, cy + dy, argb);
         //--- Blend anti-aliased pixel using coverage fraction
         else BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb);
        }
  }

//+------------------------------------------------------------------+
//| Draw rounded rect border with per-corner rounding at high res    |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawSelectiveRoundRectBorderHR(CCanvas &canvas, int x, int y, int w, int h, int radius, uint argb, int thickness,
                                                        bool rTL, bool rTR, bool rBL, bool rBR)
  {
   //--- Skip drawing when border width is disabled
   if (BorderWidth <= 0) return;
   //--- Clamp radius to half the smallest dimension
   radius = MathMin(radius, MathMin(w / 2, h / 2));
   //--- Compute per-corner radii based on rounding flags and half-thickness offset
   int tlR = rTL ? radius : 0, trR = rTR ? radius : 0, blR = rBL ? radius : 0, brR = rBR ? radius : 0, h2 = thickness / 2;
   //--- Draw top edge
   DrawBorderEdge(canvas, x + tlR,     y + h2,     x + w - trR, y + h2,     thickness, argb);
   //--- Draw right edge if any right corner is rounded
   if (rTR || rBR) DrawBorderEdge(canvas, x + w - h2, y + trR, x + w - h2, y + h - brR, thickness, argb);
   //--- Draw bottom edge
   DrawBorderEdge(canvas, x + w - brR, y + h - h2, x + blR,     y + h - h2, thickness, argb);
   //--- Draw left edge if any left corner is rounded
   if (rTL || rBL) DrawBorderEdge(canvas, x + h2, y + h - blR, x + h2, y + tlR, thickness, argb);
   //--- Draw top-left corner arc if rounded
   if (rTL) DrawCornerArc(canvas, x + radius,     y + radius,     radius, thickness, argb, M_PI,       M_PI * 1.5);
   //--- Draw top-right corner arc if rounded
   if (rTR) DrawCornerArc(canvas, x + w - radius, y + radius,     radius, thickness, argb, M_PI * 1.5, M_PI * 2.0);
   //--- Draw bottom-left corner arc if rounded
   if (rBL) DrawCornerArc(canvas, x + radius,     y + h - radius, radius, thickness, argb, M_PI * 0.5, M_PI);
   //--- Draw bottom-right corner arc if rounded
   if (rBR) DrawCornerArc(canvas, x + w - radius, y + h - radius, radius, thickness, argb, 0.0,        M_PI * 0.5);
  }

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

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

Далее "FillCornerQuadrantHR" заполняет один квадрант закругленного угла, сканируя пиксели вокруг центра угла, фильтрует по квадрантам с использованием параметров знака и применяет выборку субпикселей 4х4 вдоль изогнутого края для определения степени покрытия. Доля покрытия масштабирует альфа-канал, создавая плавные кривые без зазубрин.

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

Для отрисовки границ метод "FillQuadrilateralBorder" растеризует четырехсторонний многоугольник с помощью сканирования строк с отсортированными пересечениями ребер, а метод "DrawBorderEdge" создает перпендикулярный четырехугольник из двух конечных точек для отрисовки толстого отрезка линии через него. Утилита "IsAngleBetween" поддерживает "DrawCornerArc", которая отрисовывает толстые дуговые сегменты, используя внутреннее и внешнее кольцо радиуса с субпиксельной выборкой для сглаживания изогнутых границ. Наконец, "DrawSelectiveRoundRectBorderHR" объединяет все воедино, рисуя четыре прямых края и четыре условные угловые дуги для создания полностью закругленной границы с учетом флагов закругления для каждого угла. После этого можно определить второй класс для управления режимами тем инструмента.

Объявление и реализация класса менеджер тем оформления

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

//+------------------------------------------------------------------+
//| CLASS 2 — Manage and apply light and dark theme color sets       |
//+------------------------------------------------------------------+
class CThemeManager : public CCanvasPrimitives
  {
protected:
   bool          m_isDarkTheme;   // Active theme flag: true = dark, false = light
   ThemeColorSet m_themeColors;   // Active color set for the current theme
protected:
   //--- Apply color values matching the current theme state
   void ApplyTheme();
  };

Мы объявляем класс "CThemeManager", который наследует от "CCanvasPrimitives" и вводит два защищенных члена: логический флаг, отслеживающий, активна ли темная тема, и экземпляр "ThemeColorSet", хранящий текущие значения цвета. Он также объявляет единственный защищенный метод, "ApplyTheme", отвечающий за заполнение набора цветов в зависимости от активного состояния темы. Метод реализован следующим образом:

//+------------------------------------------------------------------+
//| 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.buttonIconColor   = C'220,225,235'; // Near-white icon color
      m_themeColors.gripDotsColor     = C'90,100,120';  // Muted slate grip dots
      m_themeColors.separatorColor    = C'44,50,64';    // Dark separator line
     }
   else
     {
      //--- Apply light theme color assignments
      m_themeColors.sidebarBackground = clrWhite;        // White background
      m_themeColors.sidebarBorder     = C'30,35,45';     // Dark border
      m_themeColors.buttonIconColor   = C'40,45,58';     // Dark icon color
      m_themeColors.gripDotsColor     = C'160,170,185';  // Light gray grip dots
      m_themeColors.separatorColor    = C'210,215,225';  // Light separator line
     }
  }

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

Объявление класса реестра категорий

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

//+------------------------------------------------------------------+
//| CLASS 3 — Register and initialise all tool category definitions  |
//+------------------------------------------------------------------+
class CCategoryRegistry : public CThemeManager
  {
protected:
   CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions
protected:
   //--- Populate all category definitions with labels, icons, and tool flags
   void InitAllCategories();
  };

Мы объявляем класс "CCategoryRegistry", который наследует от "CThemeManager" и вводит защищенный массив структур "CategoryDefinition", размер которого равен "CAT_COUNT", для хранения всех записей категорий. Мы также объявляем метод "InitAllCategories", который будет отвечать за заполнение каждой записи в массиве ее меткой, шрифтом значка, кодом символа и признаком того, что категория содержит несколько инструментов. Реализация этого метода заключается в следующем:

//+------------------------------------------------------------------+
//| Populate all category definitions with labels, icons, flags      |
//+------------------------------------------------------------------+
void CCategoryRegistry::InitAllCategories()
  {
   //--- Assign Cursors category definition
   m_categories[CAT_CURSORS].categoryLabel     = "Cursors";
   m_categories[CAT_CURSORS].iconFontName      = ICON_CATEGORY_CURSORS.fontName;
   m_categories[CAT_CURSORS].iconCharCode      = ICON_CATEGORY_CURSORS.charCode;
   m_categories[CAT_CURSORS].hasMultipleTools  = false;
   //--- Assign Lines category definition
   m_categories[CAT_LINES].categoryLabel       = "Lines";
   m_categories[CAT_LINES].iconFontName        = ICON_CATEGORY_LINES.fontName;
   m_categories[CAT_LINES].iconCharCode        = ICON_CATEGORY_LINES.charCode;
   m_categories[CAT_LINES].hasMultipleTools    = true;
   //--- Assign Channels category definition
   m_categories[CAT_CHANNELS].categoryLabel    = "Channels";
   m_categories[CAT_CHANNELS].iconFontName     = ICON_CATEGORY_CHANNELS.fontName;
   m_categories[CAT_CHANNELS].iconCharCode     = ICON_CATEGORY_CHANNELS.charCode;
   m_categories[CAT_CHANNELS].hasMultipleTools = true;
   //--- Assign Pitchfork category definition
   m_categories[CAT_PITCHFORK].categoryLabel    = "Pitchfork";
   m_categories[CAT_PITCHFORK].iconFontName     = ICON_CATEGORY_PITCHFORK.fontName;
   m_categories[CAT_PITCHFORK].iconCharCode     = ICON_CATEGORY_PITCHFORK.charCode;
   m_categories[CAT_PITCHFORK].hasMultipleTools = true;
   //--- Assign Gann category definition
   m_categories[CAT_GANN].categoryLabel        = "Gann";
   m_categories[CAT_GANN].iconFontName         = ICON_CATEGORY_GANN.fontName;
   m_categories[CAT_GANN].iconCharCode         = ICON_CATEGORY_GANN.charCode;
   m_categories[CAT_GANN].hasMultipleTools     = true;
   //--- Assign Fibonacci category definition
   m_categories[CAT_FIBONACCI].categoryLabel    = "Fibonacci";
   m_categories[CAT_FIBONACCI].iconFontName     = ICON_CATEGORY_FIBONACCI.fontName;
   m_categories[CAT_FIBONACCI].iconCharCode     = ICON_CATEGORY_FIBONACCI.charCode;
   m_categories[CAT_FIBONACCI].hasMultipleTools = true;
   //--- Assign Shapes category definition
   m_categories[CAT_SHAPES].categoryLabel      = "Shapes";
   m_categories[CAT_SHAPES].iconFontName       = ICON_CATEGORY_SHAPES.fontName;
   m_categories[CAT_SHAPES].iconCharCode       = ICON_CATEGORY_SHAPES.charCode;
   m_categories[CAT_SHAPES].hasMultipleTools   = true;
   //--- Assign Annotations category definition
   m_categories[CAT_ANNOTATIONS].categoryLabel    = "Annotate";
   m_categories[CAT_ANNOTATIONS].iconFontName     = ICON_CATEGORY_ANNOTATIONS.fontName;
   m_categories[CAT_ANNOTATIONS].iconCharCode     = ICON_CATEGORY_ANNOTATIONS.charCode;
   m_categories[CAT_ANNOTATIONS].hasMultipleTools = true;
  }

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

Объявление класса слоя canvas

Четвертый класс в иерархии владеет объектами холста и управляет их жизненным циклом, предоставляя поверхности для рисования, на которых будет рисовать рендерер.

//+------------------------------------------------------------------+
//| CLASS 4 — Create, destroy, and resize all canvas layers          |
//+------------------------------------------------------------------+
class CCanvasLayer : public CCategoryRegistry
  {
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
   string  m_nameSidebar;             // Object name of the sidebar 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);
  };

Мы объявляем класс "CCanvasLayer", который наследует от "CCategoryRegistry" и вводит защищенные члены, необходимые для управления холстом. К ним относятся множитель суперсэмплирования, который управляет коэффициентом масштабирования высокого разрешения, идентификатор графика, два экземпляра CCanvas для боковой панели разрешения экрана и ее аналога высокого разрешения, используемого во время рендеринга с суперсэмплированием, а также строка, содержащая имя bitmap label-объекта. Мы также объявляем три защищенных метода: "CreateAllCanvases" для создания обоих холстов с заданными размерами, "DestroyAllCanvases" для очистки холстов и удаления их объектов графика, и "ResizeSidebarCanvases" для корректировки обоих холстов при изменении размеров панели. Реализация этих методов заключается в следующем:

//+------------------------------------------------------------------+
//| 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("CCanvasLayer: 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("CCanvasLayer: Failed to create sidebar 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 working canvas
   m_canvasSidebarHighRes.Destroy();
  }

//+------------------------------------------------------------------+
//| Resize both sidebar canvases to the given dimensions             |
//+------------------------------------------------------------------+
void CCanvasLayer::ResizeSidebarCanvases(int w, int h)
  {
   //--- Resize the display-resolution sidebar canvas
   m_canvasSidebar.Resize(w, h);
   //--- Update the chart object dimensions to match
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XSIZE, w);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YSIZE, h);
   //--- Resize the high-resolution canvas scaled by the supersample factor
   m_canvasSidebarHighRes.Resize(w * m_supersampleFactor, h * m_supersampleFactor);
  }

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

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

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

Объявление класса компоновки боковой панели

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

//+------------------------------------------------------------------+
//| 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

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();
  };

Мы объявляем класс "CSidebarLayout", который наследует от "CCanvasLayer" и вводит защищенные члены, определяющие геометрию боковой панели. К ним относятся положение панели, ширина и высота боковой панели, размер кнопок категорий и вертикальный отступ между ними, радиус скругления углов, высота области захвата заголовка, текущее состояние выравнивания привязки и количество максимально видимых кнопок категорий, которые помещаются в доступное пространство. Мы также объявляем четыре защищенных метода: "CalcSidebarHeight" используется для вычисления высоты панели на основе пространства графика, "CalcCategoryButtonY" — для определения вертикального положения кнопки по ее индексу, а "CalcClipTop" и "CalcClipBottom" — для определения верхней и нижней границ отсечения области кнопок категорий. Реализация этих методов заключается в следующем:

//+------------------------------------------------------------------+
//| 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;
   //--- 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;
   //--- Clamp sidebar height between minimum and available space
   m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH));
   //--- 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;
   //--- Compute how many buttons fit within the available button area
   m_sidebarMaxVisibleCats = (fullBtnH <= btnAreaH) ? CAT_COUNT
      : MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding)));
  }

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

//+------------------------------------------------------------------+
//| 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;
  }

Мы реализуем метод "CalcSidebarHeight", в котором сначала получаем текущую высоту графика с помощью ChartGetInteger, затем определяем константы вертикального отступа и устанавливаем расстояние между кнопками. Мы закрепляем панель на фиксированном вертикальном смещении и соответствующим образом обновляем положение объекта графика. Отсюда вычисляем три значения высоты: идеальную естественную высоту, необходимую для размещения всех кнопок категорий, минимальную высоту, вмещающую как минимум три кнопки, и максимальную доступную высоту, исходя из оставшегося пространства графика. Мы ограничиваем высоту боковой панели между минимальным и доступным значениями с помощью MathMax и MathMin, гарантируя, что панель всегда помещается на графика без выхода за его пределы. Наконец, мы вычисляем, сколько кнопок категорий может поместиться в используемой области кнопок, и сохраняем это число для последующего использования во время рендеринга.

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

Объявление и реализация класса рендерер боковой панели

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

//+------------------------------------------------------------------+
//| CLASS 6 — Draw and composite all sidebar visual elements         |
//+------------------------------------------------------------------+
class CSidebarRenderer : public CSidebarLayout
  {
protected:
   //--- Draw and composite the full sidebar onto its canvas
   void DrawSidebar();
   //--- Draw the header grip strip at high resolution
   void DrawHeaderStripHR(int canvasW, int canvasH);
   //--- Draw a single category button indicator at high resolution
   void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot);
   //--- Draw icon glyphs and separator lines onto the display canvas
   void DrawSidebarIconLabels();
  };

//+------------------------------------------------------------------+
//| Draw and composite the full sidebar onto its canvas              |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebar()
  {
   //--- Compute high-resolution canvas dimensions
   int ws = m_sidebarWidth * m_supersampleFactor, hs = m_sidebarHeight * m_supersampleFactor;
   //--- Resize high-res canvas if dimensions have changed
   if (m_canvasSidebarHighRes.Width() != ws || m_canvasSidebarHighRes.Height() != hs)
      m_canvasSidebarHighRes.Resize(ws, hs);
   //--- Clear the high-res canvas to fully transparent
   m_canvasSidebarHighRes.Erase(0x00000000);
   //--- Compute background alpha from opacity input
   uchar bgA = (uchar)(255 * BackgroundOpacity);
   //--- Determine which corners are rounded based on snap state
   bool rTL = (m_snapState != SNAP_LEFT), rBL = rTL;
   bool rTR = (m_snapState != SNAP_RIGHT), rBR = rTR;
   //--- Fill sidebar background with selective rounded corners
   FillSelectiveRoundRectHR(m_canvasSidebarHighRes, 0, 0, ws, hs,
                             m_panelCornerRadius * m_supersampleFactor,
                             ColorToARGB(m_themeColors.sidebarBackground, bgA), rTL, rTR, rBL, rBR);
   //--- Draw left snap flush edge using foreground chart color
   if (m_snapState == SNAP_LEFT)
      m_canvasSidebarHighRes.FillRectangle(0, 0, m_supersampleFactor - 1, hs - 1,
         ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255));
   //--- Draw right snap flush edge using foreground chart color
   else if (m_snapState == SNAP_RIGHT)
      m_canvasSidebarHighRes.FillRectangle(ws - m_supersampleFactor, 0, ws - 1, hs - 1,
         ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255));
   //--- Draw the header grip strip onto the high-res canvas
   DrawHeaderStripHR(ws, hs);
   //--- Draw each category button indicator at high resolution
   for (int c = 0; c < CAT_COUNT; c++)
      DrawCategoryButtonHR(m_canvasSidebarHighRes,
         (m_sidebarWidth - m_categoryButtonSize) / 2 * m_supersampleFactor,
         CalcCategoryButtonY(c) * m_supersampleFactor,
         m_categoryButtonSize * m_supersampleFactor,
         m_categories[c].hasMultipleTools);
   //--- Draw the panel border at high resolution if enabled
   if (BorderWidth > 0)
      DrawSelectiveRoundRectBorderHR(m_canvasSidebarHighRes, 0, 0, ws, hs,
                                      m_panelCornerRadius * m_supersampleFactor,
                                      ColorToARGB(m_themeColors.sidebarBorder, 255),
                                      BorderWidth * m_supersampleFactor, rTL, rTR, rBL, rBR);
   //--- Downsample high-res canvas into the display-resolution canvas
   DownsampleCanvas(m_canvasSidebar, m_canvasSidebarHighRes, m_supersampleFactor);
   //--- Overlay icon glyphs and separator lines onto the display canvas
   DrawSidebarIconLabels();
   //--- Flush the display canvas to the chart
   m_canvasSidebar.Update();
  }

//+------------------------------------------------------------------+
//| Draw the header grip strip at high resolution                    |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawHeaderStripHR(int canvasW, int canvasH)
  {
   //--- Compute scaled header height and border inset
   int headerH = m_headerGripHeight * m_supersampleFactor;
   int brd = BorderWidth * m_supersampleFactor;
   //--- Compute horizontal insets based on snap state
   int inL = (m_snapState == SNAP_LEFT) ? 0 : brd, inR = (m_snapState == SNAP_RIGHT) ? 0 : brd;
   //--- Compute header fill rectangle position and size
   int hx = inL, hy = brd, hw = canvasW - inL - inR, hh = headerH - brd;
   //--- Compute inner corner radius inset from border
   int innerR = MathMax(0, m_panelCornerRadius * m_supersampleFactor - brd);
   //--- Determine top corner rounding based on snap state
   bool rTL = (m_snapState != SNAP_LEFT), rTR = (m_snapState != SNAP_RIGHT);
   //--- Select header fill color based on active theme
   color hdrFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252';
   //--- Fill the upper portion of the header strip with rounded top corners
   FillSelectiveRoundRectHR(m_canvasSidebarHighRes, hx, hy, hw, hh, innerR, ColorToARGB(hdrFill, 255), rTL, rTR, false, false);
   //--- Fill the lower half of the header to square off the bottom
   m_canvasSidebarHighRes.FillRectangle(hx, hy + hh / 2, hx + hw - 1, headerH - 1, ColorToARGB(hdrFill, 255));
   //--- Compute grip dot row position and height
   int row2Y = m_categoryButtonSize * m_supersampleFactor, row2H = 20 * m_supersampleFactor;
   //--- Compute dot spacing and radius at high resolution
   int gapX = 6 * m_supersampleFactor, dotR = 2 * m_supersampleFactor;
   uint dotColor = ColorToARGB(m_themeColors.gripDotsColor, 255);
   //--- Draw three horizontally centered grip dots
   for (int col = 0; col < 3; col++)
      m_canvasSidebarHighRes.FillCircle(canvasW / 2 + (col - 1) * gapX, row2Y + row2H / 2, dotR, dotColor);
  }

//+------------------------------------------------------------------+
//| Draw a single category button dot indicator at high resolution   |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool hasDot)
  {
   //--- Draw the multi-tool indicator dot in the bottom-right corner if applicable
   if (hasDot)
      target.FillCircle(xHR + sizeHR - 6 * m_supersampleFactor, yHR + sizeHR - 6 * m_supersampleFactor,
                         2 * m_supersampleFactor, ColorToARGB(m_themeColors.gripDotsColor, 180));
  }

//+------------------------------------------------------------------+
//| Draw icon glyphs and separator lines onto the display canvas     |
//+------------------------------------------------------------------+
void CSidebarRenderer::DrawSidebarIconLabels()
  {
   //--- Iterate over all category slots to draw icons
   for (int c = 0; c < CAT_COUNT; c++)
     {
      //--- Compute button top-left position in display-resolution coordinates
      int btnY = CalcCategoryButtonY(c), btnX = (m_sidebarWidth - m_categoryButtonSize) / 2;
      //--- Set font to the category icon font at configured size
      m_canvasSidebar.FontSet(m_categories[c].iconFontName, CategoryIconSize);
      //--- Build the icon glyph string from its character code
      string sym = CharToString(m_categories[c].iconCharCode);
      //--- Measure icon glyph dimensions for centring
      int iw = m_canvasSidebar.TextWidth(sym), ih = m_canvasSidebar.TextHeight(sym);
      //--- Draw the icon centred within the button area
      m_canvasSidebar.TextOut(btnX + (m_categoryButtonSize - iw) / 2, btnY + (m_categoryButtonSize - ih) / 2, sym, ColorToARGB(m_themeColors.buttonIconColor, 255));
     }
   //--- Compute separator line left and right extents based on snap and border
   int brd = BorderWidth;
   int sepL = (m_snapState == SNAP_LEFT) ? 0 : brd, sepR = m_sidebarWidth - 1 - ((m_snapState == SNAP_RIGHT) ? 0 : brd);
   //--- Pack separator line colors for primary and secondary lines
   uint sepCol  = ColorToARGB(m_themeColors.separatorColor, 255);
   uint sepCol2 = ColorToARGB(m_isDarkTheme ? C'45,52,66' : C'195,202,215', 255);
   //--- Draw primary separator below the header strip
   m_canvasSidebar.Line(sepL, m_headerGripHeight - 1,    sepR, m_headerGripHeight - 1,    sepCol);
   //--- Draw first secondary separator below the header row
   m_canvasSidebar.Line(sepL, m_categoryButtonSize,      sepR, m_categoryButtonSize,      sepCol2);
   //--- Draw second secondary separator below the theme toggle row
   m_canvasSidebar.Line(sepL, m_categoryButtonSize + 20, sepR, m_categoryButtonSize + 20, sepCol2);
   //--- Set font to Webdings for the close button icon
   m_canvasSidebar.FontSet("Webdings", CategoryIconSize);
   string closeSym = CharToString((uchar)114); // Webdings char 114 = close/X glyph
   //--- Measure close icon dimensions for centring
   int clW = m_canvasSidebar.TextWidth(closeSym), clH = m_canvasSidebar.TextHeight(closeSym);
   //--- Draw the close icon centred in the top header button slot
   m_canvasSidebar.TextOut((m_sidebarWidth - clW) / 2, (m_categoryButtonSize - clH) / 2, closeSym, ColorToARGB(m_themeColors.buttonIconColor, 255));
   //--- Compute row extents for the theme toggle button slot
   int row3Y = m_categoryButtonSize + 20, row3H = m_headerGripHeight - m_categoryButtonSize - 20;
   //--- Set font to Wingdings for the theme toggle icon
   m_canvasSidebar.FontSet("Wingdings", CategoryIconSize);
   string themeSym = CharToString((uchar)91); // Wingdings char 91 = sun/moon glyph
   //--- Measure theme icon dimensions for centring
   int thW = m_canvasSidebar.TextWidth(themeSym), thH = m_canvasSidebar.TextHeight(themeSym);
   //--- Draw the theme toggle icon centred in its row slot
   m_canvasSidebar.TextOut((m_sidebarWidth - thW) / 2, row3Y + (row3H - thH) / 2, themeSym, ColorToARGB(m_themeColors.buttonIconColor, 255));
  }

Во-первых, мы объявляем класс "CSidebarRenderer", который наследует от "CSidebarLayout" и вводит четыре защищенных метода рендеринга: "DrawSidebar" для полного пайплайна композиции, "DrawHeaderStripHR" для области захвата заголовка, "DrawCategoryButtonHR" для индикаторов отдельных кнопок и "DrawSidebarIconLabels" для наложения глифов значков и разделительных линий на холст отображения.

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

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

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

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

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

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

//+------------------------------------------------------------------+
//| CLASS 7 — Top-level sidebar shell exposing the public interface  |
//+------------------------------------------------------------------+
class CToolsSidebar : public CSidebarRenderer
  {
public:
   CToolsSidebar()  { InitDefaults(); }  // Construct and apply default state
   ~CToolsSidebar() { Destroy(); }       // Destruct and clean up all resources

   //--- Initialise the sidebar and build all canvas objects
   bool Init(long chartId);
   //--- Destroy all canvas objects and release resources
   void Destroy();
   //--- Handle incoming chart events (reserved for future interaction)
   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();
  };

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

//+------------------------------------------------------------------+
//| 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 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 panel position to origin
   m_panelX                = 0;
   m_panelY                = CanvasY;
   //--- Default snap state to left edge
   m_snapState             = SNAP_LEFT;
   //--- Apply the starting theme from user input
   m_isDarkTheme           = StartDark;
  }

//+------------------------------------------------------------------+
//| Initialise the sidebar and build all canvas objects              |
//+------------------------------------------------------------------+
bool CToolsSidebar::Init(long chartId)
  {
   //--- Reset all members to defaults before initialising
   InitDefaults();
   //--- Store the target chart identifier
   m_chartId = chartId;
   //--- Populate all category definitions
   InitAllCategories();
   //--- 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 panel on the chart
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX);
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY);
   //--- Set the sidebar Z-order to render above chart objects
   ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100);
   //--- Draw the initial sidebar frame
   DrawSidebar();
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and release resources                 |
//+------------------------------------------------------------------+
void CToolsSidebar::Destroy()
  {
   //--- Delegate canvas cleanup to the layer base class
   DestroyAllCanvases();
  }

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

Метод "Init" управляет всей последовательностью запуска. Сначала мы вызываем "InitDefaults", чтобы обеспечить чистое состояние, затем сохраняем идентификатор графика и вызываем "InitAllCategories" для заполнения реестра категорий. Мы применяем цвета активной темы, вычисляем высоту боковой панели на основе доступного пространства графика и создаем оба слоя холста. Если создание холста не удается, мы возвращаем false, чтобы сообщить о неудаче. В противном случае мы размещаем боковую панель на графике с помощью ObjectSetInteger, устанавливаем ее z-порядок так, чтобы она отображалась поверх других объектов графика, и вызываем функцию "DrawSidebar" для отрисовки начального кадра.

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

Объявление глобального экземпляра боковой панели

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

//+------------------------------------------------------------------+
//| Global variables                                                 |
//+------------------------------------------------------------------+
CToolsSidebar g_sidebar; // Global sidebar instance

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

DOT OPERATOR ACCESS ILLUSTRATION

С учетом этого, мы можем перейти к инициализации программы.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   //--- Initialise the sidebar and fail if canvas creation fails
   if (!g_sidebar.Init(ChartID())) return INIT_FAILED;
   //--- Force a chart redraw to display the sidebar immediately
   ChartRedraw();
//---
   return(INIT_SUCCEEDED);
  }

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

PALETTE INITIALIZATION

После этого нам нужно будет обработать события графика и логику деинициализации.

Обработка деинициализации и событий графика

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

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   //--- Destroy the sidebar and remove all canvas objects
   g_sidebar.Destroy();
   //--- Force a chart redraw to clear the sidebar from view
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert chart event function                                      |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lp, const double &dp, const string &sp)
  {
//---
   //--- Forward the chart event to the sidebar event handler
   g_sidebar.OnEvent(id, lp, dp, sp);
  }

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

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


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

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

TOOLS PALETTE BACKTEST GIF

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


Заключение

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

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

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

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

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

Прикрепленные файлы |
Рыночные секреты Ларри Уильямса (Часть 13): Автоматизация разворотных паттернов Hidden Smash Day Рыночные секреты Ларри Уильямса (Часть 13): Автоматизация разворотных паттернов Hidden Smash Day
В статье создается прозрачный советник MQL5 для скрытых разворотов Smash Day Ларри Уильямса. Сигналы формируются только на новых барах: сначала проверяется сетап-бар, затем он подтверждается, когда следующая сессия торгуется за его экстремумом. Риск управляется через ATR или структурные стопы с заданным соотношением риска и прибыли, размер позиции может быть фиксированным или рассчитываться от баланса, а фильтры направления и правило одной позиции помогают обеспечить воспроизводимость тестов.
От начального до среднего уровня: Произвольный доступ (II) От начального до среднего уровня: Произвольный доступ (II)
В этой статье мы рассмотрим, как два немного отличающихся подхода могут существенно повлиять на всю методологию реализации как с точки зрения производительности, так и с точки зрения проектирования дискового ввода-вывода с целью предотвращения проблем совместимости между различными приложениями.
Разработка инструментария для анализа Price Action (Часть 67): Автоматизация мониторинга уровней поддержки и сопротивления в MQL5 Разработка инструментария для анализа Price Action (Часть 67): Автоматизация мониторинга уровней поддержки и сопротивления в MQL5
В этой статье реализован полноценный советник на языке MQL5, который в реальном времени отслеживает вручную построенные уровни поддержки и сопротивления. Он синхронизирует горизонтальные линии, обнаруживает приближения, касания, пробои, развороты и ретесты, а также выполняет проверку свечных паттернов при необходимости. Алерты и маркеры на графике обеспечивают четкую и воспроизводимую обратную связь, позволяя сохранить ручной анализ и при этом автоматизировать отслеживание ключевых ценовых уровней.
Рыночные секреты Ларри Уильямса (Часть 12): Торговля разворотами Smash Day на основе контекста Рыночные секреты Ларри Уильямса (Часть 12): Торговля разворотами Smash Day на основе контекста
В статье показано, как автоматизировать разворотные паттерны Smash Day Ларри Уильямса в MQL5 в рамках структурированного подхода. Мы реализуем советник, который ограничивает период валидности сетапов заданным окном, согласует входы с направлением тренда по Supertrend и применяет фильтры по дням недели, а также поддерживает вход при пересечении уровня или после закрытия бара за уровнем. Код обеспечивает правило: не более одной открытой позиции одновременно и поддерживает расчет объема на основе риска или фиксированный размер позиции. Приведены пошаговая разработка, процедура бэктестирования и воспроизводимые настройки.