English
preview
Торговые инструменты на MQL5 (Часть 34): Замена штатных графических объектов интерактивным слоем рисования на холсте

Торговые инструменты на MQL5 (Часть 34): Замена штатных графических объектов интерактивным слоем рисования на холсте

MetaTrader 5 — Торговые системы |
23 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

В 32-й части серии "Палитра инструментов" мы создали работающую боковую панель с выдвижными панелями, прокруткой, привязкой, переключением тем, инструментом "Перекрестие" с лупой, а также систему размещения объектов, которая по щелчку мыши создавала штатные графические объекты MetaTrader. Однако эта система не позволяла пользователю изменять объекты после размещения: не было ни перетаскивания, ни управления маркерами, ни предпросмотра с растягиваемым контуром, ни возможности удалить конкретный объект. Эта статья полностью восполняет этот пробел. Мы рассмотрим следующие темы:

  1. От штатных графических объектов к архитектуре рисования на холсте
  2. Создание графических примитивов и инструментов рисования линий
  3. Реализация движка рисования и управления объектами
  4. Проверка попадания, выделение и взаимодействие с объектами
  5. Визуализация
  6. Заключение

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


От штатных графических объектов к архитектуре рисования на холсте

В части 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).

GIF с результатами тестирования

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


Заключение

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

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

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


Вложения

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

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

Особенности написания Пользовательских Индикаторов Особенности написания Пользовательских Индикаторов
Написание пользовательских индикаторов в торговой системе MetaTrader 4
Генетическое программирование — Genetic Programming (GP) Генетическое программирование — Genetic Programming (GP)
Разбираем алгоритм Genetic Programming (GP) — классический метод, который выращивает не числа, а формулы. Почему знаменитый алгоритм в каноническом виде проигрывает обычному случайному поиску во всех тестах, и какая одна строка возвращает ему силу? Ответ касается любого оптимизатора с пропорциональным отбором. В конце статьи — готовый код GP на MQL5, который можно сразу использовать для своих задач.
Особенности написания экспертов Особенности написания экспертов
Написание и тестирование экспертов в торговой системе MetaTrader 4.
Конструирование признаков для машинного обучения (Часть 12): Фрактальные признаки в MQL5 Конструирование признаков для машинного обучения (Часть 12): Фрактальные признаки в MQL5
При прямом переносе детектора фракталов на MQL5 каждый паттерн записывается на центральном баре, поэтому буфер, считываемый советником (EA), содержит значение, которое стало доступно лишь на n баров позже. Мы реализуем CFractalFeatures.mqh с методом ProcessBar для побарной обработки и методом Compute для пересчёта всего ряда; реализация включает обнаружение фракталов, оценку их силы относительно фиксированного или масштабируемого по волатильности минимального порога силы, кольцевой буфер событий для уровней поддержки и сопротивления, а также сигналы пробоя с фильтрацией по тренду. Результат — восемнадцать буферов, значения в которых публикуются на баре подтверждения и проверены на соответствие эталонной реализации на Python с точностью до 1e-13.