Торговые инструменты на MQL5 (Часть 34): Замена штатных графических объектов интерактивным слоем рисования на холсте
Введение
В MetaQuotes Language 5 (MQL5) для создания системы рисования, работающей как профессиональный инструмент построения графиков, недостаточно просто размещать штатные графические объекты по щелчку мыши. Штатные графические объекты (трендовые линии, горизонтальные линии, прямоугольники) негибки. После размещения возможности программного взаимодействия ограничены: нет проверки попадания с пиксельной точностью, перетаскивания собственных маркеров и контроля над субпиксельной отрисовкой. Эта статья предназначена для разработчиков MQL5 и алгоритмических трейдеров, которые хотят создавать интерактивные инструменты рисования профессионального уровня непосредственно на холсте графика с полным контролем над отрисовкой, выделением и манипулированием объектами.
В 32-й части серии "Палитра инструментов" мы создали работающую боковую панель с выдвижными панелями, прокруткой, привязкой, переключением тем, инструментом "Перекрестие" с лупой, а также систему размещения объектов, которая по щелчку мыши создавала штатные графические объекты MetaTrader. Однако эта система не позволяла пользователю изменять объекты после размещения: не было ни перетаскивания, ни управления маркерами, ни предпросмотра с растягиваемым контуром, ни возможности удалить конкретный объект. Эта статья полностью восполняет этот пробел. Мы рассмотрим следующие темы:
- От штатных графических объектов к архитектуре рисования на холсте
- Создание графических примитивов и инструментов рисования линий
- Реализация движка рисования и управления объектами
- Проверка попадания, выделение и взаимодействие с объектами
- Визуализация
- Заключение
К концу статьи мы получим систему рисования, в которой все линии, прямоугольники и аннотации размещаются на специально выделенном слое холста, реагируют на действия мыши и могут с высокой точностью выделяться, менять форму, перемещаться и удаляться.
От штатных графических объектов к архитектуре рисования на холсте
В части 32 каждый инструмент рисования создавал штатный графический объект MetaTrader в тот момент, когда пользователь щелкал по графику. Трендовая линия становилась объектом OBJ_TREND, горизонтальная линия — объектом OBJ_HLINE, а прямоугольник — объектом OBJ_RECTANGLE. MetaTrader полностью управлял их жизненным циклом, отрисовкой и положением. Такой подход годится для простого размещения объектов, но передаёт управление терминалу, который взамен предоставляет очень мало возможностей. Вы не можете с точностью до пикселя определить, над каким объектом находится указатель мыши, перехватить перетаскивание конкретного маркера или отрисовать собственные круглые маркеры, предпросмотр с растягиваемым пунктирным контуром и плавающие информационные панели так, как это делает профессиональный инструмент рисования.
В части 34 мы заменяем штатные графические объекты растровым холстом, охватывающим весь график. Он располагается над содержимым графика, но под боковой и выдвижными панелями. Каждая линия, прямоугольник и аннотация хранятся в нашем собственном массиве структур объектов. При перерисовке графика мы перебираем элементы этого массива и отрисовываем каждый объект на холсте пиксель за пикселем с помощью собственных процедур отрисовки. Поскольку мы управляем отрисовкой, мы можем выполнять проверку попадания с точностью до пикселя. Такой контроль позволяет реализовать выделение, перетаскивание, работу с маркерами, предпросмотр с растягиваемым контуром и удаление.
Реализация состоит из трёх уровней, каждый из которых опирается на предыдущий. Первый уровень — основа отрисовки: функции рисования графических примитивов и библиотека отрисовки линий для каждого инструмента. Второй уровень — управление объектами: структура для хранения каждого рисуемого объекта, управление жизненным циклом холста и движок перерисовки. Третий уровень — взаимодействие: система проверки попадания и инструмент "Указатель", который преобразует попадание в выделение, перетаскивание или удаление. Каждый уровень зависит от предыдущего, а вместе они заменяют все возможности, которые прежде предоставляли штатные графические объекты. Вот краткая наглядная схема того, чего мы хотим достичь.

Создание графических примитивов и инструментов рисования линий
Подготовка базовых графических примитивов
Файл "ToolsPalette_Primitives.mqh" служит основой всего стека отрисовки. Каждый класс в системе наследуется от него, поэтому всё, что определено здесь, доступно повсеместно. Для начала мы подключаем библиотеку холста MQL5 и определяем три входных параметра и структуру "ThemeColorSet", необходимые для работы пользовательского интерфейса.
//+------------------------------------------------------------------+ //| ToolsPalette_Primitives.mqh | //| Copyright 2026, Allan Munene Mutiiria.| //| https://t.me/Forex_Algo_Trader| //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_PRIMITIVES_MQH #define TOOLS_PALETTE_PRIMITIVES_MQH #include <Canvas/Canvas.mqh> //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input int BorderWidth = 1; // Border Width (px) input double BackgroundOpacity = 0.92; // Background Opacity (0.0 - 1.0) input bool StartDark = false; // Start In Dark Theme //+------------------------------------------------------------------+ //| Theme color set structure | //+------------------------------------------------------------------+ struct ThemeColorSet { color sidebarBackground; // Sidebar panel fill color color sidebarBorder; // Sidebar outer border color color buttonHoverBackground; // Button fill on hover color buttonActiveBackground; // Button fill when active/pressed color buttonIconColor; // Icon tint in normal state color buttonIconActiveColor; // Icon tint when button is active color flyoutBackground; // Flyout panel fill color color flyoutBorder; // Flyout outer border color color flyoutItemHoverBackground; // Flyout row fill on hover color flyoutTextColor; // Flyout item label text color color flyoutTextActiveColor; // Flyout item label text when active color flyoutTitleColor; // Flyout section title text color color gripDotsColor; // Drag-grip dot fill color color closeButtonHoverColor; // Close button tint on hover color themeButtonHoverColor; // Theme-toggle button tint on hover color separatorColor; // Divider line color color accentBarColor; // Accent / highlight bar color color scrollArrowColor; // Scroll arrow fill in normal state color scrollArrowHoverColor; // Scroll arrow fill on hover };
Эти три входных параметра определяют внешний вид всей палитры: ширина рамки задаёт толщину всех рамок панелей в пикселях, непрозрачность фона определяет, насколько график виден сквозь фон боковой и выдвижных панелей, а флаг тёмной темы — какая тема загружается при инициализации. Структура "ThemeColorSet" объединяет все девятнадцать цветовых токенов, которым в светлой и тёмной темах присваиваются разные значения. Вместо того чтобы распределять отдельные переменные цвета по нескольким классам, мы группируем их в одну структуру, чтобы одним вызовом функции "ApplyTheme" можно было сразу заменить всю палитру. Мы переносим эту структуру без изменений из предыдущей версии в отдельный файл. Следующим шагом будет определение нескольких вспомогательных функций, которые упростят интеграцию с холстом.
Новые глобальные вспомогательные функции отрисовки
В этой версии введён набор глобальных свободных функций, определённых вне классов и доступных всем процедурам отрисовки в системе. Мы определяем их перед иерархией классов, чтобы даже самые низкоуровневые методы графических примитивов могли их вызывать. Начнём со вспомогательной функции преобразования непрозрачности, функции смешивания пикселей, функции отрисовки толстых линий со сглаживанием и функции формирования шаблонов стилей линий.
//+------------------------------------------------------------------+ //| Convert color + percentage opacity to ARGB uint | //+------------------------------------------------------------------+ uint ColorWithPercentOpacity(color c, int pct) { //--- Clamp percentage to valid range if(pct < 0) pct = 0; if(pct > 100) pct = 100; //--- Map percentage to 0-255 alpha byte const uchar alpha = (uchar)((255 * pct) / 100); //--- Return ARGB with computed alpha return ColorToARGB(c, alpha); } //+------------------------------------------------------------------+ //| Alpha-composite a single pixel onto the canvas (source-over) | //+------------------------------------------------------------------+ void WidgetBlendPixel(CCanvas &canvas, int x, int y, uint srcArgb) { //--- Reject pixels outside canvas bounds if(x < 0 || y < 0 || x >= canvas.Width() || y >= canvas.Height()) return; //--- Extract source alpha const uchar sa = (uchar)((srcArgb >> 24) & 0xFF); //--- Skip fully transparent pixels if(sa == 0) return; //--- Write opaque pixels directly without blending overhead if(sa == 255) { canvas.PixelSet(x, y, srcArgb); return; } //--- Read destination pixel for compositing const uint dst = canvas.PixelGet(x, y); //--- Unpack destination alpha const uchar da = (uchar)((dst >> 24) & 0xFF); //--- Unpack source RGB channels const int sr = (int)((srcArgb >> 16) & 0xFF); const int sg = (int)((srcArgb >> 8) & 0xFF); const int sb = (int)( srcArgb & 0xFF); //--- Unpack destination RGB channels const int dr = (int)((dst >> 16) & 0xFF); const int dg = (int)((dst >> 8) & 0xFF); const int db = (int)( dst & 0xFF); //--- Compute output alpha via source-over formula const int oa = sa + (int)da * (255 - sa) / 255; //--- Discard pixel if resulting alpha is zero if(oa <= 0) { canvas.PixelSet(x, y, 0); return; } //--- Precompute (1 - srcAlpha) factor for destination contribution const int oneMinusSA = 255 - sa; //--- Blend each channel proportionally const int r = (sr * sa + dr * (int)da * oneMinusSA / 255) / oa; const int g = (sg * sa + dg * (int)da * oneMinusSA / 255) / oa; const int b = (sb * sa + db * (int)da * oneMinusSA / 255) / oa; //--- Pack blended channels back into ARGB and write const uint outArgb = ((uint)oa << 24) | ((uint)(uchar)r << 16) | ((uint)(uchar)g << 8) | (uint)(uchar)b; canvas.PixelSet(x, y, outArgb); } //+------------------------------------------------------------------+ //| Draw an anti-aliased thick line between two points | //+------------------------------------------------------------------+ void WidgetThickLineAA(CCanvas &canvas, int x0, int y0, int x1, int y1, int thickness, uint argb) { //--- Clamp thickness to supported range if(thickness < 1) thickness = 1; if(thickness > 4) thickness = 4; if(y0 == y1) { //--- Compute horizontal span limits const int xL = MathMin(x0, x1); const int xR = MathMax(x0, x1); //--- Center the stroke vertically around y0 const int yTop = y0 - thickness / 2; const int yBot = yTop + thickness - 1; //--- Fill every pixel in the aligned rectangle for(int yy = yTop; yy <= yBot; yy++) for(int xx = xL; xx <= xR; xx++) WidgetBlendPixel(canvas, xx, yy, argb); return; } if(x0 == x1) { //--- Compute vertical span limits const int yT = MathMin(y0, y1); const int yB = MathMax(y0, y1); //--- Center the stroke horizontally around x0 const int xL = x0 - thickness / 2; const int xR = xL + thickness - 1; //--- Fill every pixel in the aligned rectangle for(int xx = xL; xx <= xR; xx++) for(int yy = yT; yy <= yB; yy++) WidgetBlendPixel(canvas, xx, yy, argb); return; } //--- General diagonal path — true AA via signed distance + subpixel coverage const double halfT = (double)thickness / 2.0; //--- Shift pixel centers to their geometric midpoints const double ax = (double)x0 + 0.5; const double ay = (double)y0 + 0.5; const double bx = (double)x1 + 0.5; const double by = (double)y1 + 0.5; //--- Compute segment direction vector const double dx = bx - ax; const double dy = by - ay; const double lenSq = dx * dx + dy * dy; //--- Skip degenerate zero-length segments if(lenSq < 1e-9) return; //--- Compute bounding box with padding for AA radius const double pad = halfT + 1.0; const int bbL = (int)MathFloor(MathMin(ax, bx) - pad); const int bbT = (int)MathFloor(MathMin(ay, by) - pad); const int bbR = (int)MathCeil (MathMax(ax, bx) + pad); const int bbB = (int)MathCeil (MathMax(ay, by) + pad); //--- Extract base alpha and RGB from input color const uchar bA = (uchar)((argb >> 24) & 0xFF); const uint rgb = argb & 0x00FFFFFF; //--- Set up 4x4 subpixel grid const int sub = 4; const double step = 1.0 / sub; const int subSq = sub * sub; //--- Iterate over every pixel in the bounding box for(int py = bbT; py <= bbB; py++) { for(int px = bbL; px <= bbR; px++) { //--- Project pixel center onto segment to find closest point const double pcx = (double)px + 0.5; const double pcy = (double)py + 0.5; double t = ((pcx - ax) * dx + (pcy - ay) * dy) / lenSq; //--- Clamp projection to segment endpoints if(t < 0.0) t = 0.0; if(t > 1.0) t = 1.0; const double projX = ax + t * dx; const double projY = ay + t * dy; //--- Compute perpendicular distance to segment const double pdx = pcx - projX; const double pdy = pcy - projY; const double centerDist = MathSqrt(pdx * pdx + pdy * pdy); //--- Skip pixels fully outside stroke radius + AA margin if(centerDist > halfT + 1.0) continue; //--- Write fully covered interior pixels immediately if(centerDist <= halfT - 1.0) { WidgetBlendPixel(canvas, px, py, argb); continue; } //--- Count subpixel samples inside the stroke for AA coverage int inside = 0; for(int sy = 0; sy < sub; sy++) { for(int sx = 0; sx < sub; sx++) { //--- Sample subpixel position const double sx_ = (double)px + (sx + 0.5) * step; const double sy_ = (double)py + (sy + 0.5) * step; //--- Project subpixel onto segment double st = ((sx_ - ax) * dx + (sy_ - ay) * dy) / lenSq; if(st < 0.0) st = 0.0; if(st > 1.0) st = 1.0; const double spx = ax + st * dx; const double spy = ay + st * dy; const double sdx = sx_ - spx; const double sdy = sy_ - spy; //--- Count subpixel if inside half-thickness radius if(sdx * sdx + sdy * sdy <= halfT * halfT) inside++; } } //--- Skip pixels with zero subpixel coverage if(inside == 0) continue; //--- Write pixel with alpha proportional to coverage fraction const uint covArgb = (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb; WidgetBlendPixel(canvas, px, py, covArgb); } } } //+------------------------------------------------------------------+ //| Build on/off pixel-length pattern for a given line style | //+------------------------------------------------------------------+ int BuildLineStylePattern(int lineStyle, int lineWidth, int &outPattern[]) { //--- Clamp line width to valid range if(lineWidth < 1) lineWidth = 1; if(lineWidth > 4) lineWidth = 4; switch(lineStyle) { case 1: // Dashed style ArrayResize(outPattern, 2); outPattern[0] = 6; outPattern[1] = 4; return 2; case 2: // Dotted style — gap and dash scale with width ArrayResize(outPattern, 2); outPattern[0] = lineWidth * 2; outPattern[1] = lineWidth * 2; return 2; case 3: // Dash-dot style ArrayResize(outPattern, 4); outPattern[0] = 6; outPattern[1] = 3; outPattern[2] = lineWidth; outPattern[3] = 3; return 4; default: // Solid — caller skips dashed rendering entirely ArrayResize(outPattern, 0); return 0; } }
Мы определяем функцию "ColorWithPercentOpacity" для преобразования цвета и целочисленного значения непрозрачности в процентах в упакованное значение ARGB. Мы ограничиваем процентное значение диапазоном от нуля до ста, затем преобразуем его в байт альфа-канала со значением от 0 до 255 и возвращаем результат с помощью функции ColorToARGB. Это позволяет понятно и наглядно задавать непрозрачность в каждой процедуре рисования. Вместо того чтобы вычислять байты альфа-канала вручную, мы просто передаём цвет и значение непрозрачности в процентах. Это будет очень полезно при разработке окна настроек. При перетаскивании будет использоваться тот же подход — это дополнительное преимущество, которое мы получаем уже сейчас.
Далее мы определим функцию "WidgetBlendPixel", которая выполняет альфа-композитинг одного исходного пикселя с пикселем холста по формуле Портера — Даффа "источник поверх" (source-over). Сначала мы отбрасываем любой пиксель, выходящий за границы холста. Для полностью прозрачных пикселей исходного изображения мы вообще не выполняем запись. Для полностью непрозрачных пикселей мы выполняем запись напрямую, без дополнительных затрат на смешивание. Для всех промежуточных значений альфа-канала мы считываем пиксель назначения, распаковываем каналы исходного пикселя и пикселя назначения, вычисляем выходное значение альфа-канала, пропорционально смешиваем каждый канал, упаковываем результат и записываем его обратно. Эта функция лежит в основе каждого вызова отрисовки со сглаживанием в системе.
Реализовав смешивание пикселей, мы определяем функцию "WidgetThickLineAA" для отрисовки толстой линии со сглаживанием между двумя точками. Для чисто горизонтальных или вертикальных отрезков мы используем быстрый алгоритм — просто заполняем прямоугольник, выровненный по пикселям и центрированный по оси линии. Для диагональных отрезков мы вычисляем перпендикулярное расстояние со знаком от центра каждого пикселя до отрезка и пропускаем пиксели, для которых оно по модулю превышает радиус обводки плюс один пиксель запаса для сглаживания. Полностью покрытые внутренние пиксели записываем напрямую, а для граничных пикселей выполняем проверку по субпиксельной сетке 4×4: подсчитываем, сколько из шестнадцати точек попадает в пределы радиуса обводки, и записываем пиксель со значением альфа-канала, пропорциональным доле покрытия.
Наконец, мы определяем функцию "BuildLineStylePattern", которая формирует массивы длин чередующихся закрашенных участков и промежутков в пикселях для штрихового, точечного и штрихпунктирного стилей линий. Для штрихового и точечного стилей мы возвращаем массив из двух элементов, а для штрихпунктирного — из четырёх, причём длины промежутков и штрихов масштабируются в зависимости от толщины линии. Для сплошных линий мы возвращаем пустой массив, чтобы вызывающий код полностью пропускал ветвь отрисовки штриховых линий. Остальные функции, связанные с линиями и окружностями, используют тот же подход. Для отрисовки окружностей мы используем следующий подход.
Графические примитивы для отрисовки кругов и окружностей со сглаживанием
В этой версии в класс "CCanvasPrimitives" добавлены два новых метода, которых не было в предыдущей версии: метод отрисовки заполненного круга и метод отрисовки окружности. Мы добавляем их, поскольку они нужны движку рисования для отрисовки круглых маркеров на выделенных объектах и для инструмента рисования окружностей.
//+------------------------------------------------------------------+ //| Fill a circle with anti-aliased edge | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillCircleAA(CCanvas &canvas, int cx, int cy, int radius, uint argb) { //--- Cache radius as float and extract base alpha and RGB double rd = (double)radius; uchar bA = (uchar)((argb >> 24) & 0xFF); uint rgb = argb & 0x00FFFFFF; //--- Set up 4x4 subpixel AA grid int sub = 4; double step = 1.0 / sub; int subSq = sub * sub; //--- Iterate over bounding box with one-pixel AA margin for(int dy = -radius - 1; dy <= radius + 1; dy++) for(int dx = -radius - 1; dx <= radius + 1; dx++) { //--- Compute radial distance from circle center double dist = MathSqrt((double)(dx * dx + dy * dy)); //--- Skip pixels fully outside the circle if(dist > rd + 1.0) continue; //--- Write fully covered interior pixels directly if(dist <= rd - 1.0) { canvas.PixelSet(cx + dx, cy + dy, argb); continue; } //--- Count subpixel samples inside the circle radius int inside = 0; for(int sy = 0; sy < sub; sy++) for(int sx = 0; sx < sub; sx++) { //--- Compute subpixel offset from pixel corner 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 pixels with zero subpixel coverage if(inside == 0) continue; //--- Blend AA boundary pixel with proportional alpha BlendPixelSet(canvas, cx + dx, cy + dy, (((uint)(uchar)((int)bA * inside / subSq)) << 24) | rgb); } } //+------------------------------------------------------------------+ //| Draw a circle border with anti-aliasing | //+------------------------------------------------------------------+ void CCanvasPrimitives::DrawCircleBorderAA(CCanvas &canvas, int cx, int cy, int radius, int thickness, uint argb) { //--- Delegate to DrawCornerArc using a full 360-degree sweep DrawCornerArc(canvas, cx, cy, radius, thickness, argb, 0.0, M_PI * 2.0); }
Сначала мы определяем функцию "FillCircleAA" для заливки круга с аккуратно сглаженной границей. Мы кэшируем радиус в виде числа с плавающей запятой и извлекаем базовое значение альфа-канала и компоненты RGB из упакованного значения цвета. Затем мы перебираем пиксели ограничивающего прямоугольника, который выходит на один пиксель за пределы радиуса во всех направлениях, чтобы учесть запас для сглаживания. Для каждого пикселя мы вычисляем его радиальное расстояние от центра окружности с помощью функции MathSqrt. Пиксели, расстояние до которых превышает радиус более чем на один пиксель, пропускаются полностью. Пиксели, расстояние до которых меньше радиуса более чем на один пиксель, записываются напрямую без дополнительных затрат на смешивание. Для граничных пикселей — тех, которые находятся на расстоянии не более одного пикселя от края, — мы используем ту же субпиксельную сетку 4×4, что и для сглаживания линий: подсчитываем, сколько из шестнадцати отсчётов попадает внутрь круга, и с помощью функции "BlendPixelSet" смешиваем пиксель с содержимым холста, задавая альфа-значение пропорционально доле покрытия.
Далее мы определяем функцию "DrawCircleBorderAA" для рисования сглаженного контура круга заданной толщины. Вместо того чтобы заново реализовывать логику растризации кольца, мы полностью передаём эту задачу существующей функции "DrawCornerArc", задавая полный угловой диапазон в 360 градусов: от нуля до M_PI * 2.0. Таким образом мы получаем кольцо с единообразным сглаживанием, используя тот же механизм отрисовки дуг, который рисует закруглённые углы боковой панели и выдвижных панелей, без дублирования кода. Это позволяет нам получить маркеры активных объектов, показанные ниже.

После этого мы перейдем к файлу инструментов и рассмотрим процедуры рисования на холсте для каждого инструмента. В классе инструментов линий мы добавили несколько новых функций-членов.
Класс инструментов линий
Теперь, когда графические примитивы готовы, перейдём к файлу "ToolsPalette_Lines.mqh", в котором сосредоточены все процедуры рисования линий на холсте и проверки попадания в системе. Здесь мы определяем класс "CLineTools", наследующийся от "CCrosshairManager", чтобы он правильно вписывался в цепочку наследования.
//+------------------------------------------------------------------+ //| ToolsPalette_Lines.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_LINES_MQH #define TOOLS_PALETTE_LINES_MQH //--- Include crosshair manager as the base class in the inheritance chain #include "ToolsPalette_Crosshair.mqh" //+------------------------------------------------------------------+ //| CLineTools — line tools library: draw and hit-test primitives | //+------------------------------------------------------------------+ class CLineTools : public CCrosshairManager { protected: //--- Handle display state written by CDrawingEngine before each per-tool draw call int m_currentObjIsActive; // Whether this object is selected or hovered (0 or 1) int m_hideHandleIdx; // Handle index to suppress during active drag (-1 = none) int m_haloHandleIdx; // Handle index to render with hover halo (-1 = none) //--- Last-drawn info panel bounding rect for HitTestInfoLinePanel (-1 = no panel drawn) int m_lastInfoPanelX1; // Info panel left pixel int m_lastInfoPanelY1; // Info panel top pixel int m_lastInfoPanelX2; // Info panel right pixel int m_lastInfoPanelY2; // Info panel bottom pixel //--- Generic handle and geometry helpers void DrawHandleOnCanvas(CCanvas &canvas, int x, int y, bool selected, color objColor, bool showHalo = false); void ExtendLineToEdges(int canvasW, int canvasH, int x1, int y1, int x2, int y2, int &ox1, int &oy1, int &ox2, int &oy2, bool leftExtend, bool rightExtend); double PointToSegmentDistance(int mx, int my, int x1, int y1, int x2, int y2); //--- Per-line-tool canvas drawing routines (canvas-agnostic, pass target canvas explicitly) void DrawTrendLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawHorizontalLineOn(CCanvas &canvas, int y, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawVerticalLineOn(CCanvas &canvas, int x, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawCrossLineOn(CCanvas &canvas, int cx, int cy, // Full-canvas H + V intersecting at (cx, cy) color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawRayLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawExtendedLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawInfoLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, datetime t1, datetime t2, double p1, double p2, bool selected, bool hovered, bool isDarkTheme, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); void DrawTrendAngleOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, bool isDarkTheme, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0); //--- Per-line-tool hit-testing (explicit threshold for caller control) bool HitTestTrendLine(int mx, int my, int x1, int y1, int x2, int y2, int threshold); bool HitTestHorizontalLine(int mx, int my, int y, int threshold); bool HitTestVerticalLine(int mx, int my, int x, int threshold); bool HitTestInfoLinePanel(int mx, int my); //--- Canvas icon renderer for the tool palette buttons bool DrawToolIconOnCanvas(CCanvas &canvas, int toolType, int cx, int cy, int size, color iconColor); //--- Rectangle draw for this version (30% translucent fill, 2px border, 8 handles) void DrawRectangleOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0, color fillColor = clrNONE, int fillOpacity = 30); };
В начале класса мы объявляем три поля, хранящие состояние отображения маркеров. Первое поле отслеживает, активен ли объект, который в данный момент рисуется. Второе поле хранит индекс маркера, который нужно скрыть во время перетаскивания: когда пользователь захватывает маркер, мы скрываем его, чтобы он казался приподнятым над холстом. Третье поле хранит индекс маркера, который нужно отрисовать с ореолом при наведении, чтобы пользователь получал чёткую визуальную обратную связь при приближении курсора к перетаскиваемой точке. Кроме того, мы объявляем четыре целочисленные переменные, хранящие координаты ограничивающего прямоугольника последней отрисованной панели информационной линии; система проверки попадания использует их, чтобы определить, попал ли щелчок мыши внутрь плавающего окна статистики.
Вслед за полями состояния мы объявляем полный набор защищённых методов. Вспомогательные геометрические функции обеспечивают отрисовку маркеров, продление линий до краёв холста и вычисление расстояния от точки до отрезка. Процедуры рисования для отдельных инструментов — "DrawTrendLineOn", "DrawHorizontalLineOn", "DrawVerticalLineOn", "DrawCrossLineOn", "DrawRayLineOn", "DrawExtendedLineOn", "DrawInfoLineOn" и "DrawTrendAngleOn" — принимают явно заданный целевой холст, что позволяет им рисовать на любой поверхности, а не только на основном холсте рисования. Все они принимают параметры ширины линии, непрозрачности и стиля с разумными значениями по умолчанию, чтобы движок мог переопределять их для каждого объекта.
Методы проверки попадания охватывают трендовые, горизонтальные и вертикальные линии, а также панель информационной линии. Мы также объявляем функцию "DrawToolIconOnCanvas" для отрисовки векторных значков на плитках боковой панели и функцию "DrawRectangleOn" для инструмента "Прямоугольник" с полупрозрачной заливкой (30% непрозрачности), настраиваемой границей и восемью маркерами в состоянии выделения. Итак, у всех инструментов есть маркеры, поэтому давайте начнём с логики их отрисовки.
Отрисовка маркеров опорных точек
У каждого нарисованного объекта при выделении или наведении курсора в опорных точках отображаются круглые маркеры. Мы определяем функцию "DrawHandleOnCanvas" для отрисовки этих маркеров с полным сглаживанием и необязательным эффектом ореола при наведении.
//+------------------------------------------------------------------+ //| Draw an anti-aliased circle handle at (x, y) with optional halo | //+------------------------------------------------------------------+ void CLineTools::DrawHandleOnCanvas(CCanvas &canvas, int x, int y, bool selected, color objColor, bool showHalo) { //--- All handles use the same dodger-blue border regardless of object color int radius = 7; int borderPx = selected ? 2 : 1; // Thicker border when selected double outerR = (double)radius; double innerR = outerR - (double)borderPx; color handleBorderColor = clrDodgerBlue; uint fillARGB = ColorToARGB(clrWhite, 255); uint bordARGB = ColorToARGB(handleBorderColor, 255); int cW = canvas.Width(), cH = canvas.Height(); //--- Supersample with a 4x4 grid per pixel for anti-aliased edges int sub = 4; double step = 1.0 / sub; int subSq = sub * sub; //--- Pass 0: optional hover halo — semi-transparent filled disc at ~2x radius if(showHalo) { double haloR = outerR * 2.0 - 2.0; // Slightly under 2x for subtlety uchar haloR_ = (uchar)((handleBorderColor) & 0xFF); uchar haloG_ = (uchar)((handleBorderColor >> 8) & 0xFF); uchar haloB_ = (uchar)((handleBorderColor >> 16) & 0xFF); int hRint = (int)(haloR + 1); for(int dy = -hRint; dy <= hRint; dy++) { for(int dx = -hRint; dx <= hRint; dx++) { int px = x + dx, py = y + dy; //--- Skip pixels outside the canvas if(px < 0 || px >= cW || py < 0 || py >= cH) continue; //--- Count supersample points inside the halo disc int inHalo = 0; for(int sy = 0; sy < sub; sy++) for(int sx = 0; sx < sub; sx++) { double sdx = dx - 0.5 + (sx + 0.5) * step; double sdy = dy - 0.5 + (sy + 0.5) * step; if(sdx*sdx + sdy*sdy <= haloR*haloR) inHalo++; } if(inHalo > 0) { //--- Scale alpha 70 by sub-pixel coverage fraction uint a = (uint)(70 * inHalo / subSq); if(a > 0) { uint haloPixel = (a << 24) | ((uint)haloR_ << 16) | ((uint)haloG_ << 8) | (uint)haloB_; BlendPixelSet(canvas, px, py, haloPixel); } } } } } //--- Pass 1: fill interior with white using sub-pixel coverage for(int dy = -(radius+1); dy <= radius+1; dy++) for(int dx = -(radius+1); dx <= radius+1; dx++) { int px = x + dx, py = y + dy; if(px < 0 || px >= cW || py < 0 || py >= cH) continue; //--- Count supersample points inside the inner fill disc int inFill = 0; for(int sy = 0; sy < sub; sy++) for(int sx = 0; sx < sub; sx++) { double sdx = dx - 0.5 + (sx + 0.5) * step; double sdy = dy - 0.5 + (sy + 0.5) * step; if(sdx*sdx + sdy*sdy <= innerR*innerR) inFill++; } if(inFill > 0) BlendPixelSet(canvas, px, py, (((uint)(uchar)(255 * inFill / subSq)) << 24) | (fillARGB & 0x00FFFFFF)); } //--- Pass 2: border ring on top of fill using sub-pixel coverage for(int dy = -(radius+1); dy <= radius+1; dy++) for(int dx = -(radius+1); dx <= radius+1; dx++) { int px = x + dx, py = y + dy; if(px < 0 || px >= cW || py < 0 || py >= cH) continue; //--- Count supersample points in the ring between innerR and outerR int inBord = 0; for(int sy = 0; sy < sub; sy++) for(int sx = 0; sx < sub; sx++) { double sdx = dx - 0.5 + (sx + 0.5) * step; double sdy = dy - 0.5 + (sy + 0.5) * step; double sd = sdx*sdx + sdy*sdy; if(sd > innerR*innerR && sd <= outerR*outerR) inBord++; } if(inBord > 0) BlendPixelSet(canvas, px, py, (((uint)(uchar)(255 * inBord / subSq)) << 24) | (bordARGB & 0x00FFFFFF)); } }
Мы задаём фиксированный радиус маркера в семь пикселей, а толщину границы устанавливаем равной двум пикселям, если объект выделен, и одному пикселю в остальных случаях, поэтому у маркеров выделенного объекта кольцо заметно толще. Эти значения являются произвольными и могут быть изменены в соответствии с вашими визуальными предпочтениями. Для границы мы всегда используем цвет Dodger Blue независимо от цвета обводки объекта, поэтому маркеры легко распознать у инструментов любого типа. Внутренняя часть всегда белого цвета. Затем мы последовательно выполняем три прохода отрисовки.
На первом проходе мы рисуем необязательный ореол при наведении — полупрозрачный закрашенный круг с радиусом примерно вдвое больше радиуса маркера. Мы перебираем пиксели в ограничивающем прямоугольнике круга ореола, для каждого пикселя используем субпиксельную сетку 4×4, подсчитываем точки выборки, попавшие внутрь круга, и смешиваем цвет пикселя с цветом ореола, задавая альфа-значение как произведение максимального значения 70 на долю покрытия. В результате вокруг маркера появляется мягкое свечение, которое ясно показывает пользователю, что курсор находится рядом с перетаскиваемой точкой, не создавая резко очерченного видимого кольца.
На втором проходе мы закрашиваем внутреннюю часть маркера белым цветом. Мы перебираем пиксели в ограничивающем прямоугольнике внутреннего круга, используем ту же субпиксельную сетку, подсчитываем точки выборки, попавшие внутрь этого круга, и смешиваем цвет пикселей с белым, задавая альфа-значение пропорционально доле покрытия. На третьем проходе мы рисуем кольцевую границу цвета Dodger Blue поверх белой заливки. Мы перебираем пиксели в том же ограничивающем прямоугольнике, но учитываем только точки выборки между внутренним и внешним радиусами (внутри кольца) и смешиваем цвет пикселей с цветом границы, задавая альфа-значение пропорционально доле покрытия. Отрисовка границы после заливки гарантирует, что кольцо аккуратно ляжет поверх белой внутренней области без артефактов, связанных с порядком отрисовки. Мы использовали логику на основе кольца, чтобы все элементы точно совмещались. Можно также использовать соответствующий математический подход, показанный ниже, но мы выбрали более простое решение.

Используя определённые нами функции, мы теперь можем нарисовать любой инструмент. Начнём с трендовой линии.
Отрисовка трендовой линии
Теперь, когда код отрисовки маркеров и вспомогательные функции для обводки готовы, определим первую процедуру отрисовки конкретного инструмента. Мы определяем функцию "DrawTrendLineOn" для отрисовки трендовой линии между двумя опорными точками на любом целевом холсте.
//+------------------------------------------------------------------+ //| Draw a trend line between two anchor points | //+------------------------------------------------------------------+ void CLineTools::DrawTrendLineOn(CCanvas &canvas, int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered, int lineWidth = 2, int lineOpacity = 100, int lineStyle = 0) { //--- Build ARGB stroke color from object color and opacity const uint argb = ColorWithPercentOpacity(objColor, lineOpacity); //--- Clamp line width and style to valid ranges if(lineWidth < 1) lineWidth = 1; if(lineWidth > 4) lineWidth = 4; if(lineStyle < 0) lineStyle = 0; if(lineStyle > 3) lineStyle = 3; //--- Draw solid or dashed stroke depending on line style if(lineStyle == 0) { DrawThickLine(canvas, x1, y1, x2, y2, lineWidth, argb); } else { int pat[]; const int n = BuildLineStylePattern(lineStyle, lineWidth, pat); if(n > 0) WidgetDashedLineAA(canvas, x1, y1, x2, y2, lineWidth, argb, pat); else DrawThickLine(canvas, x1, y1, x2, y2, lineWidth, argb); } //--- Draw anchor handles when the object is selected or hovered if(selected || hovered) { if(m_hideHandleIdx != 0) DrawHandleOnCanvas(canvas, x1, y1, selected, objColor, m_haloHandleIdx == 0); if(m_hideHandleIdx != 1) DrawHandleOnCanvas(canvas, x2, y2, selected, objColor, m_haloHandleIdx == 1); } }
Сначала мы формируем цвет обводки в формате ARGB, передавая цвет объекта и процент непрозрачности в функцию "ColorWithPercentOpacity". Затем мы ограничиваем ширину линии значением от одного до четырёх пикселей, а индекс стиля — значением от нуля до трёх. Чтобы нарисовать сплошную линию, мы напрямую вызываем функцию "DrawThickLine". Для любого другого стиля мы вызываем функцию "BuildLineStylePattern", чтобы сформировать массив длин чередующихся штрихов и промежутков в пикселях, а затем передаём его функции "WidgetDashedLineAA", которая проходит вдоль отрезка и рисует только штрихи. Если функция построения шаблона по какой-либо причине возвращает пустой массив, вместо него мы рисуем сплошную толстую линию.
После отрисовки обводки мы проверяем, выделен ли объект или наведён ли на него курсор. Если выполняется хотя бы одно из этих условий, мы рисуем два маркера опорных точек. Перед отрисовкой каждого маркера мы сравниваем его индекс с "m_hideHandleIdx": если индексы совпадают, полностью пропускаем отрисовку этого маркера, создавая впечатление, что во время перетаскивания он приподнят над холстом. Мы также сравниваем индекс маркера с "m_haloHandleIdx" и передаём результат в "DrawHandleOnCanvas", чтобы у маркера под курсором отображался ореол при наведении. Та же схема — сначала отрисовка обводки, затем, при выполнении соответствующих условий, отрисовка маркеров с проверками скрытия и отображения ореола — применяется во всех последующих процедурах отрисовки отдельных инструментов. Теперь мы перейдем к реализации движка рисования и логики управления объектами.
Реализация движка рисования и управления объектами
Реорганизация реестра инструментов
Теперь перейдем к файлу "ToolsPalette_Tools.mqh", который в цепочке наследования находится на один уровень выше инструментов линий и управляет реестром инструментов, системой категорий и всем движком рисования. Начнём с перечислений и объявления класса "CToolRegistry".
//+------------------------------------------------------------------+ //| ToolsPalette_Tools.mqh | //| Copyright 2026, Allan Munene Mutiiria.| //| https://t.me/Forex_Algo_Trader| //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_TOOLS_MQH #define TOOLS_PALETTE_TOOLS_MQH #include "ToolsPalette_Lines.mqh" //+------------------------------------------------------------------+ //| Enumerations | //+------------------------------------------------------------------+ enum TOOL_TYPE { TOOL_NONE = 0, // No active tool TOOL_POINTER, // Default pointer cursor TOOL_CROSSHAIR, // Crosshair / measure cursor TOOL_TRENDLINE, // Two-click trendline TOOL_HLINE, // One-click horizontal line TOOL_VLINE, // One-click vertical line TOOL_RAY, // Two-click ray TOOL_EXTENDED_LINE, // Two-click extended (infinite) line TOOL_INFO_LINE, // Two-click info line TOOL_TREND_ANGLE, // Two-click angle line TOOL_CROSS_LINE, // One-click cross line TOOL_PARALLEL_CHANNEL, // Three-click parallel channel TOOL_REGRESSION_CHANNEL, // Two-click regression channel TOOL_STDDEV_CHANNEL, // Two-click standard deviation channel TOOL_PITCHFORK, // Three-click pitchfork TOOL_SCHIFF_PITCHFORK, // Three-click Schiff pitchfork TOOL_MOD_SCHIFF, // Three-click modified Schiff pitchfork TOOL_GANN_LINE, // Two-click Gann line TOOL_GANN_FAN, // Two-click Gann fan TOOL_GANN_BOX, // Two-click Gann box TOOL_FIBO_RETRACEMENT, // Two-click Fibonacci retracement TOOL_FIBO_EXPANSION, // Three-click Fibonacci expansion TOOL_FIBO_CHANNEL, // Three-click Fibonacci channel TOOL_FIBO_TIMEZONES, // Two-click Fibonacci time zones TOOL_FIBO_FAN, // Two-click Fibonacci fan TOOL_FIBO_ARCS, // Two-click Fibonacci arcs TOOL_RECTANGLE, // Two-click rectangle TOOL_TRIANGLE, // Three-click triangle TOOL_ELLIPSE, // Three-click ellipse TOOL_TEXT, // One-click text annotation TOOL_ARROW_UP, // One-click up arrow marker TOOL_ARROW_DOWN, // One-click down arrow marker TOOL_THUMB_UP, // One-click thumbs-up marker TOOL_THUMB_DOWN, // One-click thumbs-down marker TOOL_PRICE_LABEL, // One-click price label TOOL_STOP_SIGN, // One-click stop sign marker TOOL_CHECK_MARK, // One-click check mark marker TOOL_ROTATED_RECTANGLE, // Three-click rotated rectangle (appended last — keeps other values stable) TOOL_PATH, // N-point polyline (appended last) TOOL_CIRCLE, // Two-click circle — center + border point (appended last) TOOL_ARC, // Three-click arc — chord P1-P2 + bulge point P3 (appended last) TOOL_CURVE, // Three-click quadratic Bézier curve, no fill (appended last) TOOL_ARROW, // Two-click annotation arrow — line + filled triangle head (appended last) //--- Planned annotation tools — enum values reserved so category //--- registration shows all intended entries; each places a simple //--- single-click marker until full drawing logic is added. TOOL_ARROW_MARKER, // Single-click arrow marker TOOL_NOTE, // Single-click note icon TOOL_PRICE_NOTE, // Single-click price-line note TOOL_CALLOUT, // Single-click callout marker TOOL_COMMENT // Single-click comment marker }; enum ENUM_CATEGORY { CAT_NONE = -1, // No category selected CAT_CURSORS = 0, // Cursor tools category CAT_LINES, // Line tools category CAT_SHAPES, // Shape tools category //--- Action categories: not real tool categories CAT_DELETE, // Delete all drawings action (action category) CAT_COUNT // Total number of registered categories }; //+------------------------------------------------------------------+ //| CLASS 4 — Tool registry and category definitions | //+------------------------------------------------------------------+ class CToolRegistry : public CLineTools { protected: CategoryDefinition m_categories[CAT_COUNT]; // All registered categories and their tool lists TOOL_TYPE m_lastUsedToolPerCategory[CAT_COUNT]; // Last selected tool per category protected: //--- Populate all categories and their tool lists void InitAllCategoriesAndTools(); //--- Append a single tool entry to a category tool array void AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip); //--- Return the category that owns the given tool type ENUM_CATEGORY GetCategoryForActiveTool(TOOL_TYPE activeTool); //--- Return the number of chart clicks required to place the given tool int GetRequiredClickCount(TOOL_TYPE toolType); //--- Return the display label string for the given tool type string GetToolLabel(TOOL_TYPE toolType); //--- Initialize last-used slots to the first tool of each category void InitLastUsedTools(); //--- Record a tool selection so its category tile shows that tool's icon on next render void RecordToolSelection(TOOL_TYPE toolType); //--- Resolve which tool the sidebar tile should display for the given category TOOL_TYPE GetCategoryDisplayToolType(ENUM_CATEGORY cat); //--- Return true for action categories that invoke a one-shot action instead of activating a tool bool IsActionCategory(ENUM_CATEGORY cat); //--- Render the canvas icon for an action category at (cx, cy) fitting inside a size × size box bool DrawActionCategoryIconOnCanvas(CCanvas &canvas, ENUM_CATEGORY cat, int cx, int cy, int size, color iconColor); };
Перечисление "TOOL_TYPE" сохраняет все значения типов инструментов из предыдущей версии и добавляет несколько новых. Мы добавляем "TOOL_TREND_ANGLE" для линии измерения угла, "TOOL_CROSS_LINE" — для перекрёстной линии, проходящей через весь холст, "TOOL_ROTATED_RECTANGLE" — для повёрнутого прямоугольника, создаваемого тремя щелчками, "TOOL_PATH" — инструмент "Контур" для полилинии с переменным числом точек, "TOOL_CIRCLE" — для окружности, задаваемой центром и точкой на границе, "TOOL_ARC" — для дуги, создаваемой тремя щелчками и определяемой хордой и точкой, задающей выпуклость, "TOOL_CURVE" — для квадратичной кривой Безье, создаваемой тремя щелчками, и "TOOL_ARROW" — для стрелки-аннотации, создаваемой двумя щелчками. Мы также резервируем значения для планируемых инструментов аннотирования — "TOOL_NOTE", "TOOL_PRICE_NOTE", "TOOL_CALLOUT" и "TOOL_COMMENT", — чтобы при регистрации категорий учитывались все запланированные инструменты ещё до полной реализации логики их рисования. Мы добавляем все новые значения в конец перечисления, чтобы сохранить неизменными числовые значения всех существующих инструментов.
Перечисление "ENUM_CATEGORY" было значительно сокращено по сравнению с предыдущей версией. Теперь мы регистрируем только три активные категории инструментов — "CAT_CURSORS", "CAT_LINES" и "CAT_SHAPES", а также вводим одну категорию действий — "CAT_DELETE". Категории действий — это новая концепция в данной версии: щелчок по плитке такой категории не открывает выдвижную панель инструментов, а сразу выполняет однократную операцию. Категория "CAT_DELETE" удаляет все нарисованные объекты с холста одним щелчком мыши.
В классе "CToolRegistry" мы добавляем два члена, которых не было в предыдущей версии. Первый — это "m_lastUsedToolPerCategory", массив, который отслеживает последний инструмент, выбранный пользователем в каждой категории, благодаря чему на плитке боковой панели всегда отображается значок последнего использованного инструмента, а не статичный значок категории. Второй — набор новых методов для поддержки этой функции: "RecordToolSelection" записывает данные в массив при активации инструмента, "GetCategoryDisplayToolType" считывает из него данные при отрисовке плитки боковой панели, "InitLastUsedTools" при инициализации заполняет каждый элемент массива первым инструментом соответствующей категории, "IsActionCategory" возвращает значение true для "CAT_DELETE" и любых будущих категорий действий, а "DrawActionCategoryIconOnCanvas" отрисовывает на холсте специально созданный значок корзины для плитки удаления. Далее перейдём к движку: рассмотрим структуру объектов, жизненный цикл холста для рисования и запоминание стиля для каждого инструмента; большинство соответствующих объявлений мы здесь лишь приведём предварительно для дальнейшего изложения.
Структура "DrawnObject" и объявление движка рисования
Основой системы управления объектами являются структура "DrawnObject" и класс "CDrawingEngine", который с ней работает. Мы определяем и то, и другое как основу, на которой строится всё остальное.
//+------------------------------------------------------------------+ //| DrawnObject — persistent drawing data structure | //+------------------------------------------------------------------+ struct DrawnObject { TOOL_TYPE toolType; // Type of drawing tool that created this object int id; // Unique integer identifier assigned at creation datetime time1; // Chart time of anchor point 1 double price1; // Chart price of anchor point 1 datetime time2; // Chart time of anchor point 2 double price2; // Chart price of anchor point 2 datetime time3; // Chart time of anchor point 3 (three-click tools) double price3; // Chart price of anchor point 3 datetime pathTimes[]; // Dynamic array of times for TOOL_PATH polyline double pathPrices[]; // Dynamic array of prices for TOOL_PATH polyline color objColor; // Primary stroke color bool selected; // True when the object is currently selected bool visible; // True when the object should be rendered string labelText; // Annotation text content int lineWidth; // Stroke width in pixels int lineStyle; // Stroke style index (0 = solid) color textColor; // Color used for annotation text int lineOpacity; // Stroke opacity 0–100 int textOpacity; // Text opacity 0–100 int fontSize; // Font size in points for annotation text bool bold; // True if annotation text is bold int vAlign; // Vertical text alignment (0 = top, 1 = center, 2 = bottom) int hAlign; // Horizontal text alignment (0 = left, 1 = center, 2 = right) color fillColor; // Primary fill color int fillOpacity; // Primary fill opacity 0–100 color midColor; // Midline color (channels) int midOpacity; // Midline opacity 0–100 color fillColor2; // Secondary fill color (e.g. opposite zone of a channel) int fillOpacity2; // Secondary fill opacity 0–100 bool midVisible; // True if the midline is shown int midWidth; // Midline stroke width int midStyle; // Midline stroke style double midOffset; // Midline position as a ratio between the two bounds (0.0–1.0) bool centerVisible; // True if the center line is shown (regression / stddev channel) color centerColor; // Center line color int centerOpacity; // Center line opacity 0–100 int centerWidth; // Center line stroke width int centerStyle; // Center line stroke style bool upperBandVisible; // True if the upper sigma band is shown double upperBandSigma; // Upper band sigma multiplier bool lowerBandVisible; // True if the lower sigma band is shown double lowerBandSigma; // Lower band sigma multiplier bool pearsonVisible; // True if the Pearson's R label is shown //--- Fibonacci retracement level arrays double fiboLevelRatio[]; // Level ratios (e.g. 0.0, 0.236, 0.382 …) color fiboLevelColor[]; // Per-level stroke color int fiboLevelOpacity[]; // Per-level opacity 0–100 int fiboLevelWidth[]; // Per-level stroke width int fiboLevelStyle[]; // Per-level stroke style bool fiboLevelVisible[]; // Per-level visibility flag //--- Fibonacci expansion level arrays double fibexLevelRatio[]; color fibexLevelColor[]; int fibexLevelOpacity[]; int fibexLevelWidth[]; int fibexLevelStyle[]; bool fibexLevelVisible[]; //--- Fibonacci channel level arrays double fibchLevelRatio[]; color fibchLevelColor[]; int fibchLevelOpacity[]; int fibchLevelWidth[]; int fibchLevelStyle[]; bool fibchLevelVisible[]; //--- Fibonacci time zone level arrays (integer Fib sequence stored as double) double fibtzLevelRatio[]; color fibtzLevelColor[]; int fibtzLevelOpacity[]; int fibtzLevelWidth[]; int fibtzLevelStyle[]; bool fibtzLevelVisible[]; //--- Fibonacci fan level arrays double fibfanLevelRatio[]; color fibfanLevelColor[]; int fibfanLevelOpacity[]; int fibfanLevelWidth[]; int fibfanLevelStyle[]; bool fibfanLevelVisible[]; //--- Fibonacci arc level arrays double fibarcLevelRatio[]; color fibarcLevelColor[]; int fibarcLevelOpacity[]; int fibarcLevelWidth[]; int fibarcLevelStyle[]; bool fibarcLevelVisible[]; //--- Gann fan level arrays (9 canonical ratios) double gannfanLevelRatio[]; color gannfanLevelColor[]; int gannfanLevelOpacity[]; int gannfanLevelWidth[]; int gannfanLevelStyle[]; bool gannfanLevelVisible[]; //--- Gann box level arrays double gannboxLevelRatio[]; color gannboxLevelColor[]; int gannboxLevelOpacity[]; int gannboxLevelWidth[]; int gannboxLevelStyle[]; bool gannboxLevelVisible[]; //--- Pitchfork median line properties bool medianVisible; // True if the median (handle) line is shown color medianColor; // Median line color int medianWidth; // Median line stroke width int medianStyle; // Median line stroke style //--- Pitchfork outer tine properties bool outerVisible; // True if the outer tines are shown color outerColor; // Outer tine color int outerWidth; // Outer tine stroke width int outerStyle; // Outer tine stroke style //--- Pitchfork inner tine properties bool innerVisible; // True if the inner tines are shown color innerColor; // Inner tine color int innerWidth; // Inner tine stroke width int innerStyle; // Inner tine stroke style }; //+------------------------------------------------------------------+ //| CLASS 5 — Drawing engine | //+------------------------------------------------------------------+ class CDrawingEngine : public CToolRegistry { protected: DrawnObject m_drawnObjects[]; // Dynamic array of all placed drawing objects int m_drawnObjectCount; // Count of active entries in m_drawnObjects int m_drawnObjectCounter; // Monotonically increasing ID generator int m_toolDrawingClickCount; // Number of clicks collected for the current placement datetime m_drawPoint1Time; // Chart time of the first placement click datetime m_drawPoint2Time; // Chart time of the second placement click double m_drawPoint1Price; // Chart price of the first placement click double m_drawPoint2Price; // Chart price of the second placement click datetime m_pathBuildTimes[]; // Accumulated point times for an in-progress TOOL_PATH double m_pathBuildPrices[]; // Accumulated point prices for an in-progress TOOL_PATH ulong m_pathLastClickMicros; // Microsecond timestamp of the last PATH click (double-click detection) int m_pathLastClickX; // Screen X of the last PATH click int m_pathLastClickY; // Screen Y of the last PATH click int m_hoveredObjectId; // ID of the object under the mouse pointer (-1 = none) int m_hoveredHandleIdx; // Handle index under the mouse pointer (-1 = none) int m_hoveredHandleHostId; // ID of the object that owns the hovered handle int m_selectedObjectId; // ID of the currently selected object (-1 = none) int m_draggedHandleIdx; // Handle index being dragged (-1 = none) bool m_isDraggingHandle; // True while a handle drag is in progress int m_hitThreshold; // Pixel radius used for hit-testing bool m_isDraggingObject; // True while a whole-object drag is in progress int m_dragLastMouseX; // Last recorded mouse X during an object drag int m_dragLastMouseY; // Last recorded mouse Y during an object drag ulong m_lastPointerClickMicros; // Timestamp of the last pointer click (double-click detection) int m_lastPointerClickHitId; // Object ID that was hit on the last pointer click bool m_pendingTextEditArmed; // True if a second click on the same text object will open edit mode int m_pendingTextEditObjId; // Object ID waiting for the second click to open edit mode int m_pendingTextEditStartX; // Mouse X when the pending text edit was armed int m_pendingTextEditStartY; // Mouse Y when the pending text edit was armed int m_addTextPromptObjId; // Object ID whose "add text" prompt is currently shown int m_addTextPromptX1; // Left edge of the add-text prompt hit rectangle int m_addTextPromptY1; // Top edge of the add-text prompt hit rectangle int m_addTextPromptX2; // Right edge of the add-text prompt hit rectangle int m_addTextPromptY2; // Bottom edge of the add-text prompt hit rectangle int m_addTextPromptCornerX[]; // Corner X coordinates of the add-text prompt polygon int m_addTextPromptCornerY[]; // Corner Y coordinates of the add-text prompt polygon bool m_labelHitActive; // True while a label hit rectangle is tracked int m_labelHitObjIdForLast; // Object ID associated with the most recent label hit rect int m_labelHitObjIdForSelected; // Object ID whose label hit rect is displayed as selected int m_labelHitX1; // Left edge of the tracked label hit rectangle int m_labelHitY1; // Top edge of the tracked label hit rectangle int m_labelHitX2; // Right edge of the tracked label hit rectangle int m_labelHitY2; // Bottom edge of the tracked label hit rectangle int m_labelHitCornerX[]; // Corner X coordinates of the label hit polygon int m_labelHitCornerY[]; // Corner Y coordinates of the label hit polygon bool m_isPreviewActive; // True while a rubber-band preview is being rendered int m_previewMouseX; // Current mouse X for the preview rubber band int m_previewMouseY; // Current mouse Y for the preview rubber band TOOL_TYPE m_previewToolType; // Tool type being previewed bool m_isEditingLabel; // True while in-place text editing is active string m_labelEditBuffer; // Current contents of the label edit buffer int m_labelCaretPos; // Caret character position within the edit buffer int m_labelSelectionAnchor; // Anchor position for selection range bool m_savedKeyboardControl; // Saved CHART_KEYBOARD_CONTROL state before keyboard override bool m_savedQuickNavigation; // Saved CHART_QUICK_NAVIGATION state before keyboard override bool m_keyboardOverrideActive; // True while keyboard events are captured by the label editor CCanvas m_canvasDrawings; // Full-chart canvas for drawing all objects string m_nameDrawings; // Object name of the drawings bitmap label CCanvas m_canvasObjPriceLabel1; // Price-axis label canvas for point 1 CCanvas m_canvasObjTimeLabel1; // Time-axis label canvas for point 1 CCanvas m_canvasObjPriceLabel2; // Price-axis label canvas for point 2 CCanvas m_canvasObjTimeLabel2; // Time-axis label canvas for point 2 CCanvas m_canvasObjPriceLabel3; // Price-axis label canvas for point 3 CCanvas m_canvasObjTimeLabel3; // Time-axis label canvas for point 3 string m_nameObjPriceLabel1; // Object name for price-axis label 1 string m_nameObjTimeLabel1; // Object name for time-axis label 1 string m_nameObjPriceLabel2; // Object name for price-axis label 2 string m_nameObjTimeLabel2; // Object name for time-axis label 2 string m_nameObjPriceLabel3; // Object name for price-axis label 3 string m_nameObjTimeLabel3; // Object name for time-axis label 3 bool m_objLabelsVisible; // True while at least one axis label is currently shown DrawnObject m_toolMemory[64]; // Per-tool-type style memory (indexed by TOOL_TYPE cast to int) bool m_toolMemoryValid[64]; // True for each slot that has been written at least once protected: //--- Create the full-chart drawings canvas bool CreateDrawingsCanvas(); //--- Destroy the drawings canvas and remove its chart object void DestroyDrawingsCanvas(); //--- Resize the drawings canvas to the current chart dimensions void ResizeDrawingsCanvas(); //--- Create all six axis-label canvases for point coordinates bool CreateObjLabelCanvases(); //--- Destroy all six axis-label canvases void DestroyObjLabelCanvases(); //--- Hide all axis labels without destroying their canvas objects void HideObjLabels(); //--- Update axis labels for the hovered or selected object void UpdateObjLabels(); //--- Redraw permanent price/time labels for all objects that use them void RedrawPermanentAxisLabels(); //--- Delete permanent axis labels associated with the given object ID void DeletePermanentAxisLabelsFor(int objId); //--- Add a drawn object to the storage array and return its new ID int AddDrawnObject(TOOL_TYPE toolType, datetime t1, double p1, datetime t2, double p2, datetime t3, double p3, color objColor, bool useMemory = true); //--- Remove the drawn object with the given ID from the storage array void RemoveDrawnObject(int id); //--- Remove all drawn objects and clear the canvas void ClearAllDrawnObjects(); //--- Find and return the array index for the given object ID (-1 if not found) int FindObjectIndexById(int id); //--- Reset all style properties of an object to tool-type defaults void ApplyToolDefaults(int objId); //--- Snapshot the style properties of the given object into the per-tool memory slot void CaptureToolMemory(int liveIdx); //--- Apply the stored per-tool memory style onto the newly created object at newIdx void ApplyToolMemory(int newIdx); //--- Redraw every object onto the drawings canvas and flush it void RedrawAllObjects(); //--- Draw a rectangle object onto the canvas void DrawRectangle(int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered); //--- Draw a dashed rubber-band preview line between two screen points void DrawRubberBand(int x1, int y1, int x2, int y2, TOOL_TYPE toolType, color objColor); //--- Update the stored preview mouse position for the current rubber band void UpdatePreviewMousePos(int mouseX, int mouseY); //--- Return the default stroke color for the given tool type color GetToolDefaultColor(TOOL_TYPE toolType); //--- Return true if the mouse point hits the axis-aligned rectangle bool HitTestRectangle(int mx, int my, int x1, int y1, int x2, int y2); //--- Return true if the mouse point hits the ellipse defined by center and radii bool HitTestEllipse(int mx, int my, int cx, int cy, int rx, int ry); //--- Return the handle index under the mouse for the given object, or -1 int HitTestHandles(int mx, int my, int objIdx); //--- Return the ID of the topmost object hit by the mouse, or -1 int HitTestAllObjects(int mouseX, int mouseY); //--- Return true if the screen point lies inside the rotated rectangle defined by four corners bool PointInRotatedRect(int px, int py, const int &cx[], const int &cy[]); //--- Dispatch a mouse click during an active drawing tool placement void HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction); //--- Place a single-click drawing object at the given chart coordinate void PlaceSingleClickObject(datetime t, double p, int sub, TOOL_TYPE toolType); //--- Commit a two-click drawing using the two stored anchor points void PlaceTwoClickObject(TOOL_TYPE toolType); //--- Commit an in-progress PATH polyline and activate TOOL_NONE void CommitPath(TOOL_TYPE &activeTool, string &instruction); //--- Cancel an in-progress PATH polyline without creating an object void CancelPath(); //--- Commit a three-click drawing using the two stored anchors and the given third point void PlaceThreeClickObject(datetime t3, double p3, TOOL_TYPE toolType); //--- Update hover state when the pointer tool is moved over the canvas void HandlePointerMouseMove(int mouseX, int mouseY); //--- Handle a single left-click when the pointer tool is active void HandlePointerClick(int mouseX, int mouseY); //--- Handle a double left-click when the pointer tool is active void HandlePointerDoubleClick(int mouseX, int mouseY); //--- Apply incremental mouse-delta to the object or handle being dragged void HandlePointerDragMove(int mouseX, int mouseY); //--- Finalize a drag operation and commit the new object position void HandlePointerDragRelease(); //--- Delete the currently selected object void DeleteSelectedObject(); //--- Begin label edit mode for the selected object (stub — implemented elsewhere) void StartLabelEdit() {} //--- Capture keyboard events away from the chart (stub) void BeginKeyboardOverride() {} //--- Release keyboard events back to the chart (stub) void EndKeyboardOverride() {} //--- Append a character to the label edit buffer (stub) void AppendLabelChar(string ch) {} //--- Remove the character before the caret in the edit buffer (stub) void BackspaceLabelChar() {} //--- Commit the current label edit buffer to the object (stub) void CommitLabel() {} //--- Discard the label edit buffer and restore the previous text (stub) void CancelLabel() {} //--- Insert a character at the current caret position (stub) void InsertCharAtCaret(string ch) {} //--- Insert a newline at the current caret position (stub) void InsertNewlineAtCaret() {} //--- Delete the character before the caret (stub) void BackspaceAtCaret() {} //--- Delete the character after the caret (stub) void DeleteAtCaret() {} //--- Move the caret one character to the left (stub) void MoveCaretLeft() {} //--- Move the caret one character to the right (stub) void MoveCaretRight() {} //--- Move the caret one line up (stub) void MoveCaretUp() {} //--- Move the caret one line down (stub) void MoveCaretDown() {} //--- Move the caret to the start of the current line (stub) void MoveCaretHome() {} //--- Move the caret to the end of the current line (stub) void MoveCaretEnd() {} //--- Return true if a text selection range is active (stub) bool HasLabelSelection() { return false; } //--- Return the start and end of the current selection range (stub) bool GetLabelSelectionRange(int &outStart, int &outEnd) { outStart=0; outEnd=0; return false; } //--- Delete the selected text range from the edit buffer (stub) bool DeleteLabelSelection() { return false; } //--- Select the entire label edit buffer content (stub) void LabelSelectAll() {} //--- Clear the current text selection without moving the caret (stub) void ClearLabelSelection() {} //--- Extend the selection one character to the left (stub) void ShiftExtendCaretLeft() {} //--- Extend the selection one character to the right (stub) void ShiftExtendCaretRight() {} //--- Extend the selection one line up (stub) void ShiftExtendCaretUp() {} //--- Extend the selection one line down (stub) void ShiftExtendCaretDown() {} //--- Extend the selection to the start of the current line (stub) void ShiftExtendCaretHome() {} //--- Extend the selection to the end of the current line (stub) void ShiftExtendCaretEnd() {} //--- Compute the current caret line and column for cursor rendering (stub) void ComputeCaretLineColumn(int &outLine, int &outCol) { outLine=0; outCol=0; } //--- Split a multi-line text string into a line array (stub) void SplitTextIntoLines(const string text, string &lines[]) { ArrayResize(lines,1); lines[0]=text; } //--- Convert a line/column pair to a flat buffer offset (stub) int LineColumnToCaret(int line, int col) { return 0; } //--- Finalize any open label edit; remove empty text annotations bool FinalizeOpenLabelEdit(); //--- Cancel any in-progress multi-click placement and return true if a redraw is needed bool CancelInProgressPlacement(); //--- Deselect all objects and reset hover state; return true if a redraw is needed bool DeselectAll(); //--- Select the object with the given ID, deselecting any previous selection void SelectObjectById(int objId); //--- Set the label caret position from a mouse click inside the host text box (stub) void SetCaretFromMouseClick(int mx, int my, int hostPaddingX, int hostPaddingY, const string fontName, int fontPt, int wrapWidth = 0) {} //--- Retrieve the text layout parameters for the host object at objIdx (stub) bool GetHostTextLayout(int objIdx, int &outPadX, int &outPadY, string &outFontName, int &outFontPt, int &outWrapWidth) { return false; } //--- Resolve the current visual layout of the label editor for caret rendering (stub) bool ResolveCaretVisualLayout(string &outLines[], int &outBufOffsets[]) { return false; } //--- Draw an annotation label near the given object bounds (stub) void DrawObjectLabel(int mx, int my, int x1, int y1, int x2, int y2, const string labelText, bool selected, bool hovered, color objColor, int anchorMode = 0, bool recordHitRect = false, int wrapWidth = 0, int hostClipL = -1, int hostClipT = -1, int hostClipR = -1, int hostClipB = -1, int shapeKind = 0, double shapeRadiusA = 0.0, double shapeRadiusB = 0.0, int maxVisibleLines = 0, int shapeCenterX = 0, int shapeCenterY = 0, int availableHeight = 0, int textOpacity = 100, int fontSize = 11, bool bold = false, int vAlign = 0, int hAlign = 1) {} //--- Generate a unique chart object name for a new drawing string MakeUniqueObjectName(); //--- Property getter overloads — color, int, string, bool, double (stubs) bool GetObjectProperty(int objId, string propId, color &outValue) { outValue=clrWhite; return false; } bool GetObjectProperty(int objId, string propId, int &outValue) { outValue=0; return false; } bool GetObjectProperty(int objId, string propId, string &outValue) { outValue=""; return false; } bool GetObjectProperty(int objId, string propId, bool &outValue) { outValue=false; return false; } bool GetObjectProperty(int objId, string propId, double &outValue) { outValue=0.0; return false; } //--- Property setter overloads — color, int, string, bool, double (stubs) bool SetObjectProperty(int objId, string propId, color value, bool preview) { return false; } bool SetObjectProperty(int objId, string propId, int value, bool preview) { return false; } bool SetObjectProperty(int objId, string propId, string value, bool preview) { return false; } bool SetObjectProperty(int objId, string propId, bool value, bool preview) { return false; } bool SetObjectProperty(int objId, string propId, double value, bool preview) { return false; } //--- Snapshot the current property state of an object for undo (stub) bool SnapshotProperties(int objId) { return false; } //--- Restore properties from the last snapshot (stub) bool RestoreProperties() { return false; } //--- Discard the current property snapshot (stub) void DiscardSnapshot() {} //--- Append a new Fibonacci level to the given object (stub) bool AppendFibLevel(int objId) { return false; } //--- Remove the Fibonacci level at levelIdx from the given object (stub) bool RemoveFibLevel(int objId, int levelIdx) { return false; } //--- Return the number of placement points for the given object (stub) int GetObjectPointCount(int objId) { return 0; } //--- Get the price of the given placement point index (stub) bool GetObjectPointPrice(int objId, int pointIdx, double &outPrice) { outPrice=0.0; return false; } //--- Get the time of the given placement point index (stub) bool GetObjectPointTime(int objId, int pointIdx, datetime &outTime) { outTime=0; return false; } //--- Set the price of the given placement point index (stub) bool SetObjectPointPrice(int objId, int pointIdx, double newPrice, bool preview) { return false; } //--- Set the time of the given placement point index (stub) bool SetObjectPointTime(int objId, int pointIdx, datetime newTime, bool preview) { return false; } //--- Notify derived classes when the selected object changes (virtual no-op) virtual void OnSelectionChanged(int objId) { /* default no-op */ } };
Мы определяем структуру "DrawnObject" для хранения всех данных, необходимых для отрисовки отдельного графического объекта, проверки попадания в него и взаимодействия с ним. Первая группа полей содержит данные об идентификации и положении объекта — тип инструмента, уникальный целочисленный идентификатор и до трёх опорных точек, заданных временем и ценой. Что касается именно инструмента "Контур", мы используем два динамических массива, в которых накапливается столько вершин, сколько размещает пользователь. Вторая группа содержит параметры визуального стиля — цвет, толщину, непрозрачность и индекс стиля обводки, а также цвет и непрозрачность заливки для фигур, цвет и непрозрачность текста для аннотаций, размер шрифта, флаг полужирного начертания и выравнивание для текстовых инструментов. Третья группа содержит расширенные свойства отдельных инструментов: видимость и стиль средней линии для каналов, настройки центральной линии и полос стандартного отклонения для каналов регрессии и каналов стандартного отклонения, а также видимость метки коэффициента корреляции Пирсона R.
Последняя группа содержит восемь наборов динамических массивов уровней — по одному для уровней Фибоначчи, расширения Фибоначчи, канала Фибоначчи, временных зон Фибоначчи, веера Фибоначчи и дуг Фибоначчи, а также для веера Ганна и прямоугольника Ганна. В каждом наборе для каждого уровня хранятся коэффициент, цвет, непрозрачность, толщина, стиль и видимость. Кроме того, для вил Эндрюса мы отдельно объявляем свойства медианной линии, внешних и внутренних зубцов. Эта структура намеренно содержит много полей: каждый объект хранит полный набор возможных свойств независимо от типа инструмента, что избавляет от необходимости приведения типов или работы с объединениями в любой части движка рисования.
В объявлении класса "CDrawingEngine" мы предусматриваем все данные состояния, необходимые для управления полным жизненным циклом объекта. Мы храним нарисованные объекты в динамическом массиве, а вместе с ним — количество объектов и монотонно возрастающий счётчик идентификаторов. Для отслеживания состояния размещения мы храним число щелчков, уже полученные значения времени и цены для двух опорных точек, а также отдельные массивы для накопления вершин инструмента "Контур" и метку времени в микросекундах для распознавания двойного щелчка. Для отслеживания состояния взаимодействия мы храним идентификатор объекта под указателем мыши, индекс маркера под указателем и идентификатор объекта, которому он принадлежит, идентификатор выделенного объекта, индекс перетаскиваемого маркера, отдельные флаги перетаскивания маркера и перетаскивания объекта целиком, а также последнее зарегистрированное положение указателя мыши.
Для отслеживания состояния редактирования текста мы храним буфер редактирования метки, позицию текстового курсора, опорную позицию выделения и флаги перехвата клавиатурного ввода. Мы объявляем основной холст для рисования и имя соответствующего графического объекта, шесть холстов для меток осей и имена соответствующих графических объектов для отображения координат опорных точек на ценовой и временной осях, а также массив из 64 ячеек для запоминания стиля для каждого инструмента и массив флагов наличия сохранённого стиля, чтобы движок рисования мог применить последний использованный стиль инструмента любого типа к следующему объекту того же типа.
Методы охватывают весь жизненный цикл объекта: создание, удаление, очистку, поиск, настройку свойств по умолчанию и запоминание стиля. Они также обеспечивают управление жизненным циклом холста и метками осей, проверку попадания, обработку взаимодействий с указателем мыши, предпросмотр и диспетчеризацию размещения объектов. Методы редактирования меток пока представляют собой заглушки и будут реализованы позже. Что касается боковой панели, мы рассмотрим промежутки между категориями, разделитель групп, который мы добавили над категорией удаления, а также логику этапа отрисовки значков боковой панели, на котором теперь используются собственные значки. Самое важное — новая логика этапа отрисовки значков, которая описана ниже.
Динамические значки боковой панели и категория действий
Теперь, когда система запоминания инструментов готова, рассмотрим, как файл "ToolsPalette_Sidebar.mqh" использует её для управления значками плиток на боковой панели. Здесь следует отметить два изменения: этап отрисовки значков в функции "DrawSidebarIconLabels" и определение активной категории в функции "DrawSidebar".
//+------------------------------------------------------------------+ //| ToolsPalette_Sidebar.mqh | //| Copyright 2026, Allan Munene Mutiiria.| //| https://t.me/Forex_Algo_Trader| //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_SIDEBAR_MQH #define TOOLS_PALETTE_SIDEBAR_MQH //--- Pull in the full tool/engine stack via ToolsPalette_Tools.mqh #include "ToolsPalette_Tools.mqh" //--- EXAMPLE OF CANVAS TOOL ICON DRAWING LOGIC //+------------------------------------------------------------------+ //| Render the full sidebar panel | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebar(TOOL_TYPE activeTool) { // EXISTING LOGIC //--- Determine the active category from the current tool selection ENUM_CATEGORY activeCat = GetCategoryForActiveTool(activeTool); // EXISTING LOGIC } //+------------------------------------------------------------------+ //| Render category icons and sidebar control labels | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarIconLabels(TOOL_TYPE activeTool) { //--- EXISTING LOGIC if(IsActionCategory((ENUM_CATEGORY)c)) { //--- Action categories use a red danger color to signal destructive intent color actionIconColor = (isActive || m_hoveredCategory == (ENUM_CATEGORY)c) ? C'255,80,80' // brighter red on hover or active : C'220,53,69'; // standard danger red //--- Draw the custom action icon (e.g., trash bin for CAT_DELETE) drewCanvasIcon = DrawActionCategoryIconOnCanvas(tmpIcons, (ENUM_CATEGORY)c, tileCx, tileCy, FlyoutIconSize, actionIconColor); } else if(displayTool != TOOL_NONE) { //--- Draw the same custom canvas icon the flyout uses, at FlyoutIconSize, //--- centered within the larger tile for breathing room drewCanvasIcon = DrawToolIconOnCanvas(tmpIcons, (int)displayTool, tileCx, tileCy, FlyoutIconSize, iconColor); } //--- EXISTING LOGIC }
В функции "DrawSidebar" мы вызываем функцию "GetCategoryForActiveTool", чтобы определить, к какой категории относится текущий активный инструмент. Это не изменилось по сравнению с предыдущей версией. Изменения касаются того, что происходит на этапе отрисовки значков. Для каждой видимой плитки категории мы теперь вызываем функцию "GetCategoryDisplayToolType", чтобы определить последний использованный инструмент для этой категории, а затем пытаемся нарисовать его векторный значок с помощью функции "DrawToolIconOnCanvas". Если отрисовка значка на холсте прошла успешно, мы используем этот значок. Если функция возвращает значение false (то есть для данного типа инструмента нет собственного значка), мы, как и раньше, используем статический глиф из набора Wingdings. Это означает, что каждая плитка категории теперь отражает фактический выбор пользователя, а не просто показывает фиксированный символ.
Для категорий действий на этапе отрисовки значков используется совершенно другая ветвь логики. Для их определения мы вызываем функцию "IsActionCategory", а затем вместо "DrawToolIconOnCanvas" вызываем "DrawActionCategoryIconOnCanvas". Кроме того, для значков действий мы используем особую цветовую схему: стандартный красный цвет, сигнализирующий об опасности, в обычном состоянии и более яркий красный при наведении курсора на плитку или когда она активна. Это позволяет визуально показать, что щелчок по плитке выполняет разрушительную операцию, а не активирует инструмент рисования. В случае "CAT_DELETE" при этом на холсте отрисовывается собственный значок корзины, который мы определили в "CToolRegistry". Если по какой-либо причине отрисовка на холсте не удаётся, мы используем глиф шрифта Wingdings, зарегистрированный для этой категории. Когда мы объединяем все эти файлы, получаем следующий результат.

Мы видим полный набор отрисованных объектов. Остальные объекты категорий будут добавлены после полной реализации их функциональности. Пока реализуем взаимодействие с этими объектами, которому посвящён следующий раздел. Это обеспечит полную интерактивность и заложит основу для работы с остальными инструментами.
Проверка попадания, выделение и взаимодействие с объектами
Система проверки попадания
Теперь перейдём к файлу "ToolsPalette_Engine_Interact.mqh", в котором полностью реализована система проверки попадания и взаимодействия с объектами с помощью указателя. Начнём с двух базовых примитивов проверки попадания и основного цикла диспетчеризации, который управляет всем процессом выделения объектов.
//+------------------------------------------------------------------+ //| ToolsPalette_Engine_Interact.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_ENGINE_INTERACT_MQH #define TOOLS_PALETTE_ENGINE_INTERACT_MQH //--- Include CDrawingEngine class declaration; guard prevents double-inclusion via the main chain #include "ToolsPalette_Tools.mqh" //+------------------------------------------------------------------+ //| Test whether point (px, py) lies inside a convex rotated quad | //+------------------------------------------------------------------+ bool CDrawingEngine::PointInRotatedRect(int px, int py, const int &cx[], const int &cy[]) { //--- Track whether any cross products are positive or negative bool anyPos = false, anyNeg = false; //--- Iterate over all 4 edges of the convex quad for(int i = 0; i < 4; i++) { //--- Wrap edge index to form a closed polygon int j = (i + 1) % 4; //--- Compute edge vector from corner i to corner j int ex = cx[j] - cx[i]; int ey = cy[j] - cy[i]; //--- Compute vector from corner i to the test point int fx = px - cx[i]; int fy = py - cy[i]; //--- Compute the 2D cross product z-component long crossZ = (long)ex * fy - (long)ey * fx; //--- Record the sign of this cross product if(crossZ > 0) anyPos = true; if(crossZ < 0) anyNeg = true; //--- Both signs found — point is outside the quad if(anyPos && anyNeg) return false; } //--- All cross products had the same sign — point is inside return true; } //+------------------------------------------------------------------+ //| Hit test: rectangle body including interior fill | //+------------------------------------------------------------------+ bool CDrawingEngine::HitTestRectangle(int mx, int my, int x1, int y1, int x2, int y2) { //--- Normalize corner coordinates so min/max are unambiguous int lx = MathMin(x1, x2), rx = MathMax(x1, x2); int ty = MathMin(y1, y2), by = MathMax(y1, y2); //--- Accept any click inside the interior or threshold border band (translucent fill makes all interior valid) return (mx >= lx - m_hitThreshold && mx <= rx + m_hitThreshold && my >= ty - m_hitThreshold && my <= by + m_hitThreshold); } //--- SAME LOGIC APPLIES TO ALL OTHER OBJECTS //+------------------------------------------------------------------+ //| Main hit test loop — return object ID under cursor or -1 | //+------------------------------------------------------------------+ int CDrawingEngine::HitTestAllObjects(int mouseX, int mouseY) { int n = ArraySize(m_drawnObjects); //--- Iterate in reverse draw order so the topmost rendered object wins for(int i = n - 1; i >= 0; i--) { //--- Skip invisible objects if(!m_drawnObjects[i].visible) continue; //--- Resolve all three anchor points to screen pixel coordinates int x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0; ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time1, m_drawnObjects[i].price1, x1, y1); if(m_drawnObjects[i].time2 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time2, m_drawnObjects[i].price2, x2, y2); if(m_drawnObjects[i].time3 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time3, m_drawnObjects[i].price3, x3, y3); //--- Dispatch to the appropriate hit test based on the tool type bool hit = false; switch(m_drawnObjects[i].toolType) { //--- Line-type tools: test cursor proximity to the line segment case TOOL_TRENDLINE: case TOOL_RAY: case TOOL_EXTENDED_LINE: case TOOL_TREND_ANGLE: hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold); break; case TOOL_INFO_LINE: //--- Info line: hit on the line body OR inside the floating info panel hit = HitTestTrendLine(mouseX, mouseY, x1, y1, x2, y2, m_hitThreshold) || HitTestInfoLinePanel(mouseX, mouseY); break; case TOOL_HLINE: hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold); break; case TOOL_VLINE: hit = HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold); break; case TOOL_CROSS_LINE: //--- Cross line is an H-line plus a V-line sharing a center hit = HitTestHorizontalLine(mouseX, mouseY, y1, m_hitThreshold) || HitTestVerticalLine(mouseX, mouseY, x1, m_hitThreshold); break; case TOOL_RECTANGLE: hit = HitTestRectangle(mouseX, mouseY, x1, y1, x2, y2); break; default: break; } //--- Return the first (topmost) matching object's ID if(hit) return m_drawnObjects[i].id; } return -1; }
Мы определяем функцию "PointInRotatedRect" для проверки, находится ли точка на экране внутри выпуклого четырехугольника, заданного координатами четырёх углов. Мы последовательно обходим все четыре ребра четырёхугольника, вычисляя двумерное векторное произведение вектора каждого ребра на вектор, направленный от начала этого ребра к проверяемой точке. Если все четыре векторных произведения имеют одинаковый знак (все положительные или все отрицательные), то точка лежит внутри выпуклой фигуры. Как только среди векторных произведений обнаруживаются и положительное, и отрицательное значения, мы знаем, что точка находится снаружи, и сразу возвращаем false. Мы используем эту функцию для проверки попадания в повёрнутые текстовые метки и прямоугольники подсказок, стороны которых не параллельны координатным осям.
Для инструмента "Прямоугольник" мы определяем функцию "HitTestRectangle", которая нормализует координаты двух углов, получая корректный ограничивающий прямоугольник, а затем проверяет, попадает ли указатель мыши в этот прямоугольник, расширенный со всех сторон на величину допуска попадания. Поскольку прямоугольник отрисовывается с полупрозрачной заливкой (30% непрозрачности), покрывающей всю его внутреннюю область, любой щелчок внутри него считается попаданием — проверять только границы не требуется. Та же логика применима ко всем остальным объектам.
После того как примитивы для каждого инструмента готовы, мы определяем "HitTestAllObjects" в качестве основного цикла диспетчеризации. Мы обходим массив нарисованных объектов в порядке, обратном порядку отрисовки, чтобы при перекрытии объектов приоритет всегда имел самый верхний из них. Для каждого видимого объекта мы преобразуем все сохранённые опорные точки в экранные координаты в пикселях с помощью функции ChartTimePriceToXY, а затем выполняем соответствующую проверку попадания в зависимости от типа инструмента. Для трендовых линий, лучей, прямых и угловых линий вызывается функция "HitTestTrendLine".
Для информационной линии вызываются обе функции — "HitTestTrendLine" и "HitTestInfoLinePanel", поэтому щелчок по плавающей панели статистики также выделяет объект. Для горизонтальных линий вызывается функция "HitTestHorizontalLine", для вертикальных — "HitTestVerticalLine", а для перекрёстных — обе функции, поскольку перекрёстная линия состоит из горизонтальной и вертикальной линий с общей точкой пересечения. Прямоугольники передаются в метод "HitTestRectangle". Как только какой-либо тест возвращает значение "true", мы немедленно возвращаем ID этого объекта, не проверяя остальные объекты. Если попадание не обнаружено ни для одного объекта, мы возвращаем минус один. В части отрисовки мы рассмотрим отрисовку всех объектов, меток осей для вертикальных и горизонтальных линий, а также предпросмотра с растягиваемым контуром.
Движок отрисовки и постоянные метки осей
Теперь мы рассмотрим файл "ToolsPalette_Engine_Render.mqh", который отвечает за полный цикл перерисовки и постоянные метки-плашки на ценовой и временной осях для горизонтальных, вертикальных и перекрёстных линий.
//+------------------------------------------------------------------+ //| ToolsPalette_Engine_Render.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_ENGINE_RENDER_MQH #define TOOLS_PALETTE_ENGINE_RENDER_MQH //--- Include CDrawingEngine class declaration; guard prevents double-inclusion via the main chain #include "ToolsPalette_Tools.mqh" //+------------------------------------------------------------------+ //| Render permanent axis label pills for every HLine, VLine, Cross | //+------------------------------------------------------------------+ void CDrawingEngine::RedrawPermanentAxisLabels() { //--- Read canvas dimensions and symbol digit count int cW = m_canvasDrawings.Width(); int cH = m_canvasDrawings.Height(); int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); //--- Iterate every stored drawn object looking for line-type tools int n = ArraySize(m_drawnObjects); for(int i = 0; i < n; i++) { //--- Skip hidden objects if(!m_drawnObjects[i].visible) continue; TOOL_TYPE tt = m_drawnObjects[i].toolType; //--- Only process HLine, VLine, and Cross Line objects if(tt != TOOL_HLINE && tt != TOOL_VLINE && tt != TOOL_CROSS_LINE) continue; //--- Cross Line renders two pills (price + time); all others render one int passCount = (tt == TOOL_CROSS_LINE) ? 2 : 1; for(int pass = 0; pass < passCount; pass++) { //--- Resolve effective tool type: Cross Line pass 0 = price pill, pass 1 = time pill TOOL_TYPE effTT = tt; if(tt == TOOL_CROSS_LINE) effTT = (pass == 0) ? TOOL_HLINE : TOOL_VLINE; //--- Build pill label text: price string for HLine, time string for VLine string labelText = (effTT == TOOL_HLINE) ? DoubleToString(m_drawnObjects[i].price1, digits) : TimeToString(m_drawnObjects[i].time1, TIME_DATE | TIME_MINUTES); //--- Skip if no label text was produced if(StringLen(labelText) == 0) continue; //--- Convert the anchor time/price to screen pixel coordinates int hx = 0, hy = 0; ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time1, m_drawnObjects[i].price1, hx, hy); //--- Use the object's user-chosen color as the pill background color pillBgColor = m_drawnObjects[i].objColor; //--- Extract RGB components to compute luminance for contrast selection uchar lR = (uchar)((pillBgColor) & 0xFF); uchar lG = (uchar)((pillBgColor >> 8) & 0xFF); uchar lB = (uchar)((pillBgColor >> 16) & 0xFF); double luminance = 0.299 * lR + 0.587 * lG + 0.114 * lB; bool bgIsDark = (luminance < 128.0); //--- Pick white text on dark backgrounds, black on light backgrounds color textColor = bgIsDark ? clrWhite : clrBlack; uint pillBgARGB = ColorToARGB(pillBgColor, 255); uint pillTextARGB = ColorToARGB(textColor, 255); //--- Measure the rendered text dimensions at 9 pt Arial TextSetFont("Arial", -(9 * 10)); uint twU = 0, thU = 0; TextGetSize(labelText, twU, thU); int tw = (int)twU, th = (int)thU; //--- Compute pill dimensions with padding, enforcing a minimum size int padX = 4, padY = 2; int lw = tw + padX * 2; int lh = th + padY * 2; if(lw < 20) lw = 20; if(lh < 12) lh = 12; //--- Compute pill top-left canvas position based on line type int lx, ly; if(effTT == TOOL_HLINE) { //--- Pin to the right canvas edge, vertically centered on the line's Y lx = cW - lw; if(lx < 0) lx = 0; ly = hy - lh / 2; if(ly < 0) ly = 0; if(ly + lh > cH) ly = cH - lh; } else { //--- Pin to the bottom canvas edge, horizontally centered on the line's X lx = hx - lw / 2; if(lx < 0) lx = 0; if(lx + lw > cW) lx = cW - lw; ly = cH - lh; if(ly < 0) ly = 0; } //--- Render label text into an offscreen pixel buffer filled with the pill background uint buf[]; ArrayResize(buf, lw * lh); ArrayFill(buf, 0, lw * lh, pillBgARGB); TextOut(labelText, padX, padY, TA_LEFT | TA_TOP, buf, lw, lh, pillTextARGB, COLOR_FORMAT_ARGB_NORMALIZE); //--- Blit the offscreen buffer onto the drawings canvas at the computed position for(int py = 0; py < lh; py++) { int dy = ly + py; //--- Skip rows outside the canvas bounds if(dy < 0 || dy >= cH) continue; for(int px = 0; px < lw; px++) { int dx = lx + px; //--- Skip columns outside the canvas bounds if(dx < 0 || dx >= cW) continue; m_canvasDrawings.PixelSet(dx, dy, buf[py * lw + px]); } } //--- Draw a 1-px top and bottom border row on the pill for contrast for(int px = 0; px < lw; px++) { int dx = lx + px; if(dx < 0 || dx >= cW) continue; if(ly >= 0 && ly < cH) m_canvasDrawings.PixelSet(dx, ly, pillTextARGB); if(ly + lh - 1 >= 0 && ly + lh - 1 < cH) m_canvasDrawings.PixelSet(dx, ly + lh - 1, pillTextARGB); } //--- Draw a 1-px left and right border column on the pill for contrast for(int py = 0; py < lh; py++) { int dy = ly + py; if(dy < 0 || dy >= cH) continue; if(lx >= 0 && lx < cW) m_canvasDrawings.PixelSet(lx, dy, pillTextARGB); if(lx + lw - 1 >= 0 && lx + lw - 1 < cW) m_canvasDrawings.PixelSet(lx + lw - 1, dy, pillTextARGB); } } } } //+------------------------------------------------------------------+ //| Delete permanent axis label bitmap for a specific object ID | //+------------------------------------------------------------------+ void CDrawingEngine::DeletePermanentAxisLabelsFor(int objId) { //--- Build the legacy per-object bitmap label name string lblName = StringFormat("tp_axislbl_%d", objId); //--- Delete the stale chart object if it still exists from a prior session if(ObjectFind(0, lblName) >= 0) ObjectDelete(0, lblName); } //+------------------------------------------------------------------+ //| Redraw all stored drawn objects onto the drawings canvas | //+------------------------------------------------------------------+ void CDrawingEngine::RedrawAllObjects() { //--- Clear the entire drawings canvas to fully transparent m_canvasDrawings.Erase(0x00000000); //--- Reset per-frame prompt and label hit IDs before rendering m_addTextPromptObjId = -1; m_labelHitObjIdForSelected = -1; //--- Iterate every stored drawn object and render it int n = ArraySize(m_drawnObjects); for(int i = 0; i < n; i++) { //--- Skip hidden objects if(!m_drawnObjects[i].visible) continue; //--- Read this object's stroke color and text color color col = m_drawnObjects[i].objColor; color colText = m_drawnObjects[i].textColor; //--- Derive selection from the canonical engine ID rather than the per-object flag //--- to guarantee at most one selected object even if stale flags are present bool sel = (m_drawnObjects[i].id == m_selectedObjectId); bool hovered = (m_drawnObjects[i].id == m_hoveredObjectId); //--- Resolve all three anchor points to screen pixel coordinates int x1 = 0, y1 = 0, x2 = 0, y2 = 0, x3 = 0, y3 = 0; ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time1, m_drawnObjects[i].price1, x1, y1); if(m_drawnObjects[i].time2 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time2, m_drawnObjects[i].price2, x2, y2); if(m_drawnObjects[i].time3 != 0) ChartTimePriceToXY(m_chartId, 0, m_drawnObjects[i].time3, m_drawnObjects[i].price3, x3, y3); //--- Configure handle hide index: hide the dragged handle so it appears picked up bool isThisSelected = (m_drawnObjects[i].id == m_selectedObjectId); m_hideHandleIdx = (isThisSelected && m_isDraggingHandle) ? m_draggedHandleIdx : -1; //--- Configure handle halo index: highlight the hovered handle except during drag bool isThisHandleHost = (m_drawnObjects[i].id == m_hoveredHandleHostId); m_haloHandleIdx = (isThisHandleHost && !m_isDraggingHandle && !m_isDraggingObject) ? m_hoveredHandleIdx : -1; //--- Dispatch to the appropriate per-tool draw routine switch(m_drawnObjects[i].toolType) { case TOOL_TRENDLINE: DrawTrendLineOn(m_canvasDrawings, x1, y1, x2, y2, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_HLINE: DrawHorizontalLineOn(m_canvasDrawings, y1, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_VLINE: DrawVerticalLineOn(m_canvasDrawings, x1, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_CROSS_LINE: DrawCrossLineOn(m_canvasDrawings, x1, y1, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_RAY: DrawRayLineOn(m_canvasDrawings, x1, y1, x2, y2, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_EXTENDED_LINE: DrawExtendedLineOn(m_canvasDrawings, x1, y1, x2, y2, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_INFO_LINE: DrawInfoLineOn(m_canvasDrawings, x1, y1, x2, y2, col, m_drawnObjects[i].time1, m_drawnObjects[i].time2, m_drawnObjects[i].price1, m_drawnObjects[i].price2, sel, hovered, m_isDarkTheme, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_TREND_ANGLE: DrawTrendAngleOn(m_canvasDrawings, x1, y1, x2, y2, col, sel, hovered, m_isDarkTheme, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle); break; case TOOL_RECTANGLE: DrawRectangleOn(m_canvasDrawings, x1, y1, x2, y2, col, sel, hovered, m_drawnObjects[i].lineWidth, m_drawnObjects[i].lineOpacity, m_drawnObjects[i].lineStyle, m_drawnObjects[i].fillColor, m_drawnObjects[i].fillOpacity); break; default: break; } } //--- Reset handle display state after loop to prevent leaking into preview renders m_hideHandleIdx = -1; m_haloHandleIdx = -1; //--- Draw rubber band preview if a two-click placement is in progress after the first click if(m_isPreviewActive && m_toolDrawingClickCount == 1) { //--- Resolve the first click anchor point to screen coordinates int px1 = 0, py1 = 0; ChartTimePriceToXY(m_chartId, 0, m_drawPoint1Time, m_drawPoint1Price, px1, py1); //--- Use the tool's default commit color for the preview stroke color previewColor = GetToolDefaultColor(m_previewToolType); if(m_previewToolType == TOOL_RECTANGLE) { //--- Rectangle preview: render the full committed look (fill + border + handles) as the user drags DrawRectangleOn(m_canvasDrawings, px1, py1, m_previewMouseX, m_previewMouseY, previewColor, false, true); } else { //--- All other tools: render a generic dashed rubber-band line from P1 to the cursor DrawRubberBand(px1, py1, m_previewMouseX, m_previewMouseY, m_previewToolType, previewColor); } //--- Read the current cursor time/price for potential axis label preview use double previewPrice; datetime previewTime; int sub; ChartXYToTimePrice(m_chartId, m_previewMouseX, m_previewMouseY, sub, previewTime, previewPrice); } //--- Render permanent axis pills for HLine/VLine directly onto the drawings canvas (no flicker) RedrawPermanentAxisLabels(); //--- Flush the completed drawings canvas to screen m_canvasDrawings.Update(); //--- Update the dedicated hover/select axis label canvases for trendlines and similar tools UpdateObjLabels(); } //+------------------------------------------------------------------+ //| Draw a rectangle between two diagonal corners | //+------------------------------------------------------------------+ void CDrawingEngine::DrawRectangle(int x1, int y1, int x2, int y2, color objColor, bool selected, bool hovered) { //--- Delegate to CShapeTools::DrawRectangleOn for fill, border, and handle rendering DrawRectangleOn(m_canvasDrawings, x1, y1, x2, y2, objColor, selected, hovered); } #endif // TOOLS_PALETTE_ENGINE_RENDER_MQH //+------------------------------------------------------------------+
Мы определяем функцию "RedrawPermanentAxisLabels" для отрисовки цветных меток-плашек непосредственно на холсте рисования для каждой горизонтальной, вертикальной и перекрёстной линии в массиве объектов. Мы просматриваем все сохраненные объекты и пропускаем те из них, которые скрыты или не относятся к одному из трех типов линий. Для перекрёстной линии мы выполняем два прохода: в первом отрисовываем метку-плашку цены на правой оси, а во втором — метку-плашку времени на нижней оси. Для каждой метки-плашки мы формируем текст с помощью DoubleToString для меток цены и TimeToString для меток времени, затем преобразуем координаты опорной точки в экранные пиксели с помощью ChartTimePriceToXY.
В качестве фона метки-плашки мы используем цвет обводки самого объекта, а цвет текста вычисляем автоматически — извлекаем красную, зелёную и синюю составляющие и вычисляем яркость по стандартной формуле с учётом весов, отражающих особенности зрительного восприятия. Если яркость опускается ниже 128, мы используем белый текст; в противном случае — черный, чтобы надпись всегда была разборчива независимо от цвета объекта. С помощью "TextOut" мы выводим текст метки во внеэкранный пиксельный буфер, заполненный цветом фона метки-плашки, затем копируем этот буфер на холст рисования с учётом его границ, после чего рисуем рамку толщиной в один пиксель со всех четырёх сторон, используя цвет текста для контраста. В результате для вертикальных и горизонтальных линий мы получаем следующий вид.

Далее мы определяем функцию "RedrawAllObjects" как центральный диспетчер отрисовки. В начале каждого кадра мы очищаем весь холст рисования до полностью прозрачного состояния, сбрасываем используемые в текущем кадре идентификаторы попаданий в подсказки и метки, а затем перебираем все сохранённые объекты. Для каждого видимого объекта мы определяем состояния выделения и наведения, сравнивая идентификатор объекта с основными идентификаторами, хранящимися в движке, вместо использования флага самого объекта. Это гарантирует, что выделен будет не более одного объекта, даже при наличии устаревших флагов.
Затем мы задаём в "m_hideHandleIdx" и "m_haloHandleIdx" индексы маркера, который нужно скрыть, и маркера с ореолом при наведении, прежде чем вызвать процедуру отрисовки соответствующего инструмента, которая считывает эти значения при отрисовке маркеров. Для каждого типа инструмента мы вызываем соответствующую функцию отрисовки — "DrawTrendLineOn", "DrawHorizontalLineOn", "DrawVerticalLineOn", "DrawCrossLineOn", "DrawRayLineOn", "DrawExtendedLineOn", "DrawInfoLineOn", "DrawTrendAngleOn" или "DrawRectangleOn", — передавая сохранённые свойства стиля непосредственно из структуры объекта. После цикла обработки объектов мы сбрасываем оба индекса маркеров в -1, чтобы их значения не влияли на последующую отрисовку предпросмотра с растягиваемым контуром.
После отрисовки всех объектов мы проверяем, активен ли предпросмотр и равен ли счётчик щелчков единице: это означает, что пользователь уже задал первую опорную точку инструмента с размещением несколькими щелчками и перемещает курсор ко второй. В случае инструмента "Прямоугольник" в качестве предварительного просмотра отображается окончательный вид объекта, чтобы пользователь точно знал, какой результат получится. Для всех остальных инструментов мы вызываем функцию "DrawRubberBand", чтобы нарисовать полупрозрачную пунктирную линию от первой опорной точки до текущего положения курсора. Затем мы вызываем "RedrawPermanentAxisLabels", чтобы нарисовать метки-плашки на осях поверх обводок объектов, выводим содержимое холста на экран с помощью "Update" и, наконец, вызываем "UpdateObjLabels", чтобы обновить шесть холстов с метками осей, отображающими координаты опорных точек выделенного объекта или объекта под курсором. Предпросмотр с растягиваемым контуром теперь выглядит следующим образом:

Наконец, добавим файл оболочки, в котором сосредоточим всю ранее реализованную логику оболочки. Код оболочки отменяет незавершённое размещение при переключении инструмента, деактивирует инструменты при щелчках вне панели и обрабатывает категорию "Удалить" в обработчике щелчков по выдвижной панели. Поскольку мы определили соответствующие методы в файле инструментов, здесь нам остаётся лишь вызвать их.
Переключение инструментов и блокировка прокрутки в оболочке
Последний компонент системы находится в файле "ToolsPalette_Shell.mqh". Мы рассмотрим два аспекта поведения, которые наиболее заметно изменились по сравнению с предыдущей версией: обработку переключения инструментов и управление прокруткой графика в разных режимах работы инструментов.
//+------------------------------------------------------------------+ //| ToolsPalette_Shell.mqh | //| Copyright 2026, Allan Munene Mutiiria.| //| https://t.me/Forex_Algo_Trader| //+------------------------------------------------------------------+ #ifndef TOOLS_PALETTE_SHELL_MQH #define TOOLS_PALETTE_SHELL_MQH #include "ToolsPalette_Sidebar.mqh" //+------------------------------------------------------------------+ //| Toggle the given tool on or off | //+------------------------------------------------------------------+ void CToolsSidebar::ToggleTool(TOOL_TYPE toolType) { FinalizeOpenLabelEdit(); DeselectAll(); CancelInProgressPlacement(); //--- Hide crosshair overlays left over from a previous crosshair session CleanupCrosshairOnToolSwitch(); //--- Re-clicking the active tool or clicking Pointer toggles the tool off if(toolType == TOOL_POINTER || m_currentActiveTool == toolType) { m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; //--- Restore scroll when no tool is active ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } else { m_currentActiveTool = toolType; m_toolDrawingClickCount = 0; //--- Record this tool as the last-used for its category so the tile shows its icon RecordToolSelection(toolType); if(toolType == TOOL_CROSSHAIR) { m_currentInstruction = "Move mouse for crosshair. Double-click to measure."; ShowAllCrosshairElements(); //--- Crosshair manages its own scroll behavior ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } else if(toolType == TOOL_POINTER || toolType == TOOL_NONE) { m_currentInstruction = ""; //--- Pointer mode locks scroll to prevent chart panning during object interaction ChartSetInteger(0, CHART_MOUSE_SCROLL, false); } else { //--- Drawing tool — allow scroll between placement clicks m_currentInstruction = "Click on chart to place " + GetToolLabel(toolType) + "."; ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } } }
Здесь мы переопределяем "ToggleTool", добавляя в начало последовательность из трёх шагов очистки, которой не было в предыдущей версии. Перед активацией любого нового инструмента мы вызываем "FinalizeOpenLabelEdit", чтобы применить или отменить изменения редактируемой текстовой аннотации, "DeselectAll", чтобы сбросить текущее выделение и состояние наведения, и "CancelInProgressPlacement", чтобы отменить незавершённое размещение несколькими щелчками. Это гарантирует, что при активации нового инструмента движок рисования всегда находится в исходном состоянии ожидания.
После очистки мы вызываем "RecordToolSelection", чтобы сохранить только что активированный инструмент как последний использованный в его категории. Именно этот вызов поддерживает значки плиток боковой панели в актуальном состоянии: при каждой активации инструмента сохранённые данные обновляются, а при следующей отрисовке боковой панели изменение автоматически учитывается. Повторный щелчок по активному инструменту или щелчок по инструменту "Указатель" отключает текущий инструмент и восстанавливает прокрутку графика. При активации инструмента "Перекрестие" отображаются все элементы перекрестия и разрешается прокрутка графика, чтобы пользователь мог сдвигать график во время измерений.
При активации инструмента "Указатель" прокрутка графика блокируется установкой CHART_MOUSE_SCROLL в false — это ключевое изменение поведения по сравнению с предыдущей версией. В режиме инструмента "Указатель" график не должен смещаться при щелчках или перетаскивании, поскольку эти события полностью обрабатываются системой взаимодействия с объектами. При активации любого инструмента рисования прокрутка разрешается между щелчками размещения, чтобы пользователь мог переместить график в другую область перед заданием следующей опорной точки. Последний элемент — основной советник, который связывает все потоки обработки воедино и приведён ниже.
Основная точка входа
После подготовки всех заголовочных файлов мы завершаем реализацию основным файлом точки входа "Tools Palette Part 5.mq5", который связывает все компоненты воедино.
//+------------------------------------------------------------------+ //| Tools Palette Part 5.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 the shell header that defines CToolsSidebar and all related logic #include "ToolsPalette_Shell.mqh" //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CToolsSidebar g_sidebar; // Singleton sidebar instance managing the entire tools palette UI //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Attempt to initialize the sidebar on the current chart if(!g_sidebar.Init(ChartID())) { //--- Report initialization failure to the journal Print("ToolsPalette: Failed to initialize. Check journal for details."); //--- Abort EA load on failure return INIT_FAILED; } //--- Force a chart redraw to render the sidebar immediately after init ChartRedraw(); //--- Signal successful initialization to the terminal return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Destroy all sidebar objects and free resources on removal g_sidebar.Destroy(); //--- Refresh the chart to clear any lingering sidebar visuals ChartRedraw(); } //+------------------------------------------------------------------+ //| Expert chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Forward every chart event directly to the sidebar event dispatcher g_sidebar.OnEvent(id, lparam, dparam, sparam); } //+------------------------------------------------------------------+ //| Expert timer function | //+------------------------------------------------------------------+ void OnTimer() { //--- Drive the sidebar timer tick, which handles cursor blink during text label editing g_sidebar.OnTimer(); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //--- No tick logic required; sidebar is event-driven only } //+------------------------------------------------------------------+
Мы подключаем только файл "ToolsPalette_Shell.mqh", который подтягивает всю иерархию классов по цепочке: оболочка → боковая панель → движок рисования → реестр инструментов → инструменты рисования линий → инструмент "Перекрестие" → графические примитивы. Мы объявляем единственный глобальный экземпляр "CToolsSidebar" с именем "g_sidebar", который управляет всей палитрой инструментов на протяжении всего времени работы программы.
В обработчике события OnInit мы вызываем "g_sidebar.Init", передавая идентификатор текущего графика. Если инициализация завершилась неудачно, мы записываем сообщение в журнал и возвращаем значение "INIT_FAILED", чтобы прервать загрузку. В случае успеха мы вызываем функцию "ChartRedraw" для немедленной отрисовки боковой панели и возвращаем значение INIT_SUCCEEDED. В обработчике события OnDeinit мы вызываем "g_sidebar.Destroy", чтобы корректно удалить все холсты и графические объекты, а затем выполняем заключительный вызов ChartRedraw, чтобы убрать с графика оставшиеся визуальные элементы.
Обработчик событий OnChartEvent передаёт каждое входящее событие напрямую в "g_sidebar.OnEvent" без какой-либо фильтрации — за всё распределение событий отвечает внутренняя система маршрутизации боковой панели. Обработчик событий OnTick намеренно оставлен пустым, поскольку вся система событийно-ориентированная и не требует обработки на каждом тике. Единственное дополнение по сравнению с предыдущей версией — обработчик событий OnTimer, который вызывает "g_sidebar.OnTimer" для управления миганием текстового курсора при редактировании текстовых меток непосредственно на графике. Когда пользователь вводит текст метки, периодический таймер вызывает этот обработчик, который, в свою очередь, вызывает "RedrawAllObjects" для смены состояния мигания, чтобы текстовый курсор мигал естественно. Мы воспользуемся этим позже при реализации. На этом наша реализация завершена. Осталось только протестировать систему, и этому будет посвящен следующий раздел.
Визуализация
Мы тестируем программу на графике в MetaTrader 5 в режиме реального времени. Ниже результат работы после компиляции представлен в формате Graphics Interchange Format (GIF).

В ходе тестирования плитки на боковой панели динамически обновлялись, отображая последний использованный инструмент в каждой категории. Размещенные объекты корректно реагировали на наведение курсора и выделение, при этом в опорных точках появлялись круглые маркеры. Перетаскивание маркеров изменяло форму объектов в реальном времени, перетаскивание объекта целиком корректно изменяло его положение, а нажатие клавиши Delete немедленно удаляло выделенный объект. Предпросмотр с растягиваемым контуром точно следовал за курсором при размещении несколькими щелчками, а режим инструмента "Указатель" сохранял блокировку прокрутки графика на протяжении всех взаимодействий с объектами.
Заключение
Итак, мы заменили использовавшуюся в предыдущей версии систему рисования на основе штатных графических объектов MetaTrader движком рисования, который полностью работает на холсте и отрисовывает каждый инструмент попиксельно на отдельном растровом слое, покрывающем весь график. Мы реализовали систему постоянного хранения объектов с запоминанием стиля для каждого инструмента, полноценную инфраструктуру проверки попадания для всех зарегистрированных типов инструментов, а также инструмент "Указатель", поддерживающий выделение объектов, перетаскивание объекта целиком, управление отдельными маркерами и удаление. Кроме того, мы переработали систему категорий, добавив плитку для непосредственного выполнения удаления, внедрили динамические значки плиток боковой панели, отображающие последний использованный инструмент в каждой категории, и добавили постоянно отображаемые метки-плашки на осях для горизонтальных и вертикальных линий. Прочитав эту статью, вы сможете:
- Создать в MQL5 движок рисования на основе холста, который хранит, отрисовывает и перерисовывает объекты, сохраняя их между перерисовками и не полагаясь на штатные графические объекты
- Реализовать проверку попадания на уровне пикселей для нескольких типов инструментов и использовать её для выделения объектов, перетаскивания маркеров и перемещения объектов целиком на отдельном слое холста
- Добавить в боковую панель систему запоминания последнего использованного инструмента, чтобы плитки категорий всегда отображали последний активированный инструмент и активировали его непосредственно при щелчке
В следующей части серии мы расширим движок рисования, добавив полные семейства каналов, инструментов Фибоначчи, инструментов Ганна и вил Эндрюса, включая параллельный канал, канал регрессии, канал стандартного отклонения, все три варианта вил Эндрюса, шесть инструментов Фибоначчи и полный набор инструментов Ганна. Следите за новостями.
Вложения
| № п/п | Название | Тип | Описание |
|---|---|---|---|
| 1 | Tools Palette Part 5.mq5 | Советник | Основная точка входа в программу, которая подключает заголовочный файл оболочки, объявляет глобальный экземпляр боковой панели и передаёт ему обработку инициализации, деинициализации, событий графика, срабатываний таймера и ценовых тиков |
| 2 | ToolsPalette_Crosshair.mqh | Подключаемый файл | Содержит класс управления режимами перекрестия и измерения, логику управления жизненным циклом его холста, обработчик изменения размера графика, а также всю логику отрисовки перекрестия и обработки событий |
| 3 | ToolsPalette_Engine_Interact.mqh | Подключаемый файл | Реализует систему проверки попадания, включая проверку попадания точки в повёрнутый прямоугольник, методы проверки попадания для каждого инструмента, основной цикл диспетчеризации проверок попадания и полный набор обработчиков взаимодействия с инструментом "Указатель": перемещения мыши, щелчка, перемещения при перетаскивании, отпускания кнопки мыши при перетаскивании и удаления объекта |
| 4 | ToolsPalette_Engine_Render.mqh | Подключаемый файл | Реализует полный цикл перерисовки, включая диспетчеризацию отрисовки отдельных объектов, настройку скрытия маркеров и ореола при наведении, отрисовку предпросмотра с растягиваемым контуром, отрисовку постоянных меток-плашек на осях для горизонтальных и вертикальных линий, вывод содержимого холста на экран и обновление холстов меток осей |
| 5 | ToolsPalette_Lines.mqh | Подключаемый файл | Определяет класс линейных инструментов со всеми процедурами отрисовки на холсте для трендовых линий, горизонтальных линий, вертикальных линий, перекрёстных линий, лучей, прямых, информационных линий, линий под углом и прямоугольников, а также механизм отрисовки маркеров опорных точек, методы проверки попадания и отрисовку значков инструментов на холсте |
| 6 | ToolsPalette_Primitives.mqh | Подключаемый файл | Определяет три входных параметра, структуру набора цветов темы, определённые вне классов глобальные функции отрисовки для преобразования непрозрачности, смешивания пикселей, рисования толстых линий со сглаживанием и построения шаблонов стилей линий, а также класс графических примитивов холста с методами рисования кругов с заливкой и обводкой со сглаживанием |
| 7 | ToolsPalette_Shell.mqh | Подключаемый файл | Содержит класс диспетчера событий верхнего уровня с последовательностью очистки при переключении инструментов, управлением блокировкой прокрутки в режимах указателя и рисования, миганием текстового курсора по таймеру при редактировании меток и обработчиком щелчков на выдвижной панели категории действий |
| 8 | ToolsPalette_Sidebar.mqh | Подключаемый файл | Отвечает за компоновку и отрисовку боковой панели, включая динамические значки плиток категорий, учитывающие последний использованный инструмент, ветвь обработки категории действий с предупреждающей красной окраской и значком корзины, разделитель групп над плиткой удаления и выдвижную панель со всеми пунктами инструментов |
| 9 | ToolsPalette_Tools.mqh | Подключаемый файл | Определяет полные перечисления типов и категорий инструментов, класс реестра инструментов с определениями категорий и запоминанием последнего использованного инструмента, структуру нарисованного объекта со всеми полями стиля и массивами уровней, а также объявление класса движка рисования с полным набором методов управления жизненным циклом объектов и холстом, проверки попадания, взаимодействия с инструментом "Указатель" и редактирования меток |
| 10 | Tools Palette Part 5.zip | Архив | Готовый к распаковке архив, содержащий все девять файлов проекта в одной папке. Распакуйте архив в каталог данных терминала MetaTrader 5 — папка проекта будет размещена в MQL5/Experts/, а все файлы окажутся на своих местах, и проект будет готов к компиляции. |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22786
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.