Торговые инструменты на MQL5 (Часть 30): Боковая панель палитры инструментов с архитектурой на основе классов
Введение
На графике MetaTrader 5 уже есть панель инструментов рисования, но она построена по плоской схеме: отрисовка, компоновка, значения темы и обработчики смешаны вместе, поэтому каждая новая функция затрагивает сразу несколько частей кода. Хотите добавить категорию? Тогда придется редактировать основной цикл рендеринга. Хотите светлую или темную тему? Тогда придется просматривать разбросанные значения цвета. Хотите аккуратные скруглённые углы — жертвуете визуальным качеством. Панель работает функционально, но не масштабируется — чем больше вы добавляете инструментов, тем, тем оформления и режимов, тем выше риск, что небольшое изменение нарушит компоновку, рендеринг или поведение.
Эта статья написана для разработчиков MetaQuotes Language 5 (MQL5) и алгоритмических трейдеров, которым нужен практический способ навести в этом порядок. Мы перестраиваем панель в многоуровневую классовую архитектуру с принципом единственной ответственности, так что расширение палитры становится "добавлением записи в реестр", а не "переписыванием рендерера". Вы получите четкую постановку задачи, реализацию, которая компилируется и запускается на графике, и воспроизводимый шаблон для добавления категорий, тем и визуальных усовершенствований без необходимости вносить изменения по всей кодовой базе.
В предыдущей статье (Часть 29) мы анимировали бабочку на графике с помощью последовательного рисования, затухания крыльев и движения в полёте. В Части 30 мы возвращаемся к палитре инструментов, которую первоначально создали в Части 19, и перестраиваем ее с нуля, используя многослойную объектно-ориентированную архитектуру боковой панели, рендеринг на canvas с суперсэмплированием, управление темами и реестр категорий. Мы рассмотрим следующие темы:
- От простой панели к многоуровневой боковой панели
- Реализация средствами MQL5
- Тестирование
- Заключение
В итоге у вас будет чистая структурированная боковая панель MQL5 со скругленными углами со сглаживанием, поддержкой двух тем и модульной системой категорий, готовая для выбора инструментов, выдвижных панелей и взаимодействия перетаскиванием в будущих частях.
От простой панели к многоуровневой боковой панели
В оригинальной палитре инструментов, созданной в Части 19, вся логика была размещена в отдельных функциях и глобальных переменных. Заголовок, кнопки инструментов, обработчики рисования и цвета темы находились на одном уровне без разделения задач. Это работало для небольшого набора из восьми инструментов, но дальнейшее масштабирование означало, что изменение одной области часто требовало корректировки нескольких других, что делало код все более сложным в обслуживании и расширении.
В результате редизайна простая плоская структура заменена вертикальной боковой панелью, разработанной на основе многоуровневой иерархии классов. Каждый класс выполняет одну единственную функцию. Например: примитивы отвечают за сглаживание при отрисовке, менеджер тем управляет наборами цветов, реестр хранит категории, слой холста управляет изменением размера, класс компоновки вычисляет геометрию, а рендерер формирует окончательную боковую панель. Добавление новой категории инструментов сводится к регистрации определения, а не к переработке всего цикла рендеринга.
На графике эта компактная вертикальная боковая панель обеспечивает быстрый доступ к категориям рисования во время сеансов в реальном времени, не перекрывая ценовой график. Функция привязки фиксирует ее вплотную к краю графика, а переключение тем мгновенно подбирает палитру под фон графика. По мере добавления пользовательских групп инструментов для собственного анализа боковая панель аккуратно расширяется без конфликтов компоновки. Мы определим сопоставления значков и перечисления, объявим входные параметры и построим иерархию классов. Оболочка боковой панели верхнего уровня будет инициализировать, отображать и перенаправлять события. В двух словах, ниже показана схема того, что мы будем создавать.

Реализация средствами 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. Вы можете использовать любой шрифт или символы по своему усмотрению. Ниже показан используемый нами список.

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

Реализация методов примитивов 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", экземпляр готов к инициализации сразу после загрузки программы. Мы можем использовать этот экземпляр для доступа ко всем членам класса с помощью операции точка. Вот наглядный пример.

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

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

Во время тестирования боковая панель отображалась корректно со скругленными углами со сглаживанием как в темной, так и в светлой темах, значки категорий отображались корректно по центру, а выравнивание по привязке удерживало панель вплотную к левому краю графика с квадратными углами на привязанной стороне.
Заключение
В заключение, мы заменили произвольную, перегруженную функциями палитру на многоуровневую архитектуру боковой панели на основе классов, которая обеспечивает четкое распределение обязанностей и чистый жизненный цикл. В результате получился компилируемый модуль MQL5, который инициализируется в обработчике OnInit и полностью завершает работу в обработчике OnDeinit, отображая вертикальную боковую палитру на графике с высокоточным сэмплированием и понижением разрешения для сглаживания углов и границ, выборочным скруглением углов при привязке к левому или правому краю графика, централизованными настройками светлой и темной тем, применяемыми из одного менеджера, реестром категорий, где добавление новой группы инструментов осуществляется одной записью определения, а не несколькими изменениями в разных файлах, а также предсказуемым созданием и изменением размера холста в одном публичном классе-оболочке, который отвечает за инициализацию и отрисовку.
Эти результаты измеримы: проект компилируется, создает bitmap label-объекты, отображает боковую панель с вышеописанными характеристиками, реагирует на перерисовку графика и корректно удаляет свои объекты при деинициализации. С архитектурной точки зрения, каждый слой предоставляет доступ только к тому, что требуется следующему: примитивы поступают в менеджер тем, который, в свою очередь, передает данные в реестр, затем в слой холста, в компоновку, в рендерер и, наконец, в оболочку. После прочтения этой статьи вы сможете:
- Структурировать утилиты для создания графиков на основе холста, используя многоуровневую иерархию классов, где каждый класс отвечает за один аспект рендеринга или компоновки
- Создавать сглаженные закругленные углы и границы с помощью рендеринга с высоким разрешением и суперсэмплированием и выборочным скруглением каждого угла
- Реализовывать систему реестра категорий, которая позволяет добавлять новые группы инструментов путем заполнения определения, а не путем переработки логики рендеринга.
В следующей части мы будем развивать эту стабильную основу, добавляя полную интерактивность, включая выдвижные панели для выбора инструментов, перетаскивание и изменение размера с помощью мыши, прокручиваемые списки с индикаторами в виде ползунка, подсветку при наведении курсора, акцентные полосы активного состояния и движок рисования на графике, который размещает объекты из инструментов, созданных одним, двумя и тремя щелчками мыши. Оставайтесь с нами.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22193
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Рыночные секреты Ларри Уильямса (Часть 13): Автоматизация разворотных паттернов Hidden Smash Day
От начального до среднего уровня: Произвольный доступ (II)
Разработка инструментария для анализа Price Action (Часть 67): Автоматизация мониторинга уровней поддержки и сопротивления в MQL5
Рыночные секреты Ларри Уильямса (Часть 12): Торговля разворотами Smash Day на основе контекста
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования