English
preview
Торговые инструменты на MQL5 (Часть 32): Перекрестие, лупа и режим измерения

Торговые инструменты на MQL5 (Часть 32): Перекрестие, лупа и режим измерения

MetaTrader 5Трейдинг |
45 2
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

В своей предыдущей статье (Часть 31) мы добавили полную интерактивность в палитру инструментов. Мы реализовали выдвижную панель для выбора инструментов и обработчик событий графика, маршрутизирующий все взаимодействия с мышью и клавиатурой. Мы также добавили графический движок, поддерживающий размещение объектов одним, двумя и тремя щелчками мыши. Также были включены перетаскивание панелей с привязкой к краям, изменение размера нижнего края, прокручиваемые списки, подсветка при наведении курсора и переключение тем в реальном времени. В части 32 мы представляем класс менеджера перекрестия. Он добавляет одиннадцать слоев холста для наложения ретикула с делениями, линий перекрестий во всю ширину и высоту графика, а также меток оси цены и времени. Он также содержит круговую лупу, отображающую увеличенное содержимое свечей, и режим измерения двойным щелчком с якорными маркерами, диагональной линией и плавающей статистикой баров и пипсов. Мы рассмотрим следующие темы:

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

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


Что привносят ретикул с делениями и лупа на график

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

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

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

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

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

CROSSHAIR RETICLE AND MAGNIFIER LENS


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

Добавление входных параметров для перекрестия и лупы

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

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

//---

input int    ReticleOffset     = 30;   // Crosshair Reticle Offset (px)
input int    ReticleTickLen    = 14;   // Crosshair Reticle Tick Length (px)
input int    ReticleThickness  = 2;    // Crosshair Reticle Tick Thickness (px)
input int    MagDiameter       = 180;  // Magnifier Diameter (px)
input double MagZoom           = 3.0;  // Magnifier Zoom Factor
input int    MagOffset         = 45;   // Magnifier Offset From Cursor (px)
input int    AxisLabelFontSize = 9;   // Axis Label Font Size (pt)
input string AxisLabelFont     = "Arial"; // Axis Label Font

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

Добавление алгоритма Брезенхема для рисования линий к примитивам холста

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

//+------------------------------------------------------------------+
//| CLASS 1 — Blend and draw low-level canvas primitives             |
//+------------------------------------------------------------------+
class CCanvasPrimitives
  {
protected:
   //--- EXISTING METHODS
   
   //--- Draw a line using Bresenham's algorithm with alpha blending
   void DrawBresenhamLine(CCanvas &canvas, int x0, int y0, int x1, int y1, uint argb);
  };

//+------------------------------------------------------------------+
//| Draw a line between two points using Bresenham's algorithm       |
//+------------------------------------------------------------------+
void CCanvasPrimitives::DrawBresenhamLine(CCanvas &canvas, int x0, int y0, int x1, int y1, uint argb)
  {
   //--- Compute absolute deltas and step directions
   int dx = MathAbs(x1 - x0), dy = MathAbs(y1 - y0);
   int sx = (x0 < x1) ? 1 : -1, sy = (y0 < y1) ? 1 : -1, err = dx - dy;
   int w = canvas.Width(), h = canvas.Height();
   //--- Iterate pixel by pixel until the endpoint is reached
   while (true)
     {
      //--- Blend the current pixel onto the canvas if within bounds
      if (x0 >= 0 && x0 < w && y0 >= 0 && y0 < h) BlendPixelSet(canvas, x0, y0, argb);
      //--- Stop when the endpoint is reached
      if (x0 == x1 && y0 == y1) break;
      int e2 = 2 * err;
      if (e2 > -dy) { err -= dy; x0 += sx; }
      if (e2 <  dx) { err += dx; y0 += sy; }
     }
  }

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

Расширение слоя холста элементами перекрестия и измерения

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

//+------------------------------------------------------------------+
//| CLASS 4 — Create, destroy, and resize all canvas layers          |
//+------------------------------------------------------------------+
class CCanvasLayer : public CToolRegistry
  {
protected:
   int     m_supersampleFactor;         // Supersampling multiplier for high-res rendering
   long    m_chartId;                   // Chart identifier this layer belongs to

   CCanvas m_canvasSidebar;             // Final display-resolution sidebar canvas
   CCanvas m_canvasSidebarHighRes;      // High-resolution sidebar canvas for supersampling
   CCanvas m_canvasFlyout;              // Final display-resolution flyout canvas
   CCanvas m_canvasFlyoutHighRes;       // High-resolution flyout canvas for supersampling
   CCanvas m_canvasReticle;             // Crosshair reticle tick-mark canvas
   CCanvas m_canvasMagnifier;           // Circular magnifier lens canvas
   CCanvas m_canvasCrossVertical;       // Crosshair vertical line canvas (1 × chartH)
   CCanvas m_canvasCrossHorizontal;     // Crosshair horizontal line canvas (chartW × 1)
   CCanvas m_canvasCrossPriceLabel;     // Crosshair price axis label canvas
   CCanvas m_canvasCrossTimeLabel;      // Crosshair time axis label canvas
   CCanvas m_canvasMeasureVertical;     // Measure mode vertical anchor line canvas
   CCanvas m_canvasMeasureHorizontal;   // Measure mode horizontal anchor line canvas
   CCanvas m_canvasMeasurePriceLabel;   // Measure mode price axis label canvas
   CCanvas m_canvasMeasureTimeLabel;    // Measure mode time axis label canvas
   CCanvas m_canvasMeasureDiagonalLine; // Measure mode diagonal line canvas (chartW × chartH)

   string  m_nameSidebar;               // Object name of the sidebar bitmap label
   string  m_nameFlyout;                // Object name of the flyout bitmap label
   string  m_nameReticle;               // Object name of the reticle bitmap label
   string  m_nameMagnifier;             // Object name of the magnifier bitmap label
   string  m_nameCrossVertical;         // Object name of the crosshair vertical bitmap label
   string  m_nameCrossHorizontal;       // Object name of the crosshair horizontal bitmap label
   string  m_nameCrossPriceLabel;       // Object name of the crosshair price label bitmap
   string  m_nameCrossTimeLabel;        // Object name of the crosshair time label bitmap
   string  m_nameMeasureVertical;       // Object name of the measure vertical bitmap label
   string  m_nameMeasureHorizontal;     // Object name of the measure horizontal bitmap label
   string  m_nameMeasurePriceLabel;     // Object name of the measure price label bitmap
   string  m_nameMeasureTimeLabel;      // Object name of the measure time label bitmap
   string  m_nameMeasureDiagonalLine;   // Object name of the measure diagonal line bitmap

protected:
   //--- Create all canvas objects at the given sidebar dimensions
   bool CreateAllCanvases(int w, int h);
   //--- Destroy all canvas objects and remove chart objects
   void DestroyAllCanvases();
   //--- Resize both sidebar canvases to the given dimensions
   void ResizeSidebarCanvases(int w, int h);
   //--- Fill the crosshair vertical line canvas with the foreground colour
   void DrawCrossVerticalLinePixels(int chartH);
   //--- Fill the crosshair horizontal line canvas with the foreground colour
   void DrawCrossHorizontalLinePixels(int chartW);
   //--- Fill the measure vertical line canvas with the foreground colour at reduced opacity
   void DrawMeasureVerticalLinePixels(int chartH);
   //--- Fill the measure horizontal line canvas with the foreground colour at reduced opacity
   void DrawMeasureHorizontalLinePixels(int chartW);
  };

//+------------------------------------------------------------------+
//| Create all canvas objects at the given sidebar dimensions        |
//+------------------------------------------------------------------+
bool CCanvasLayer::CreateAllCanvases(int w, int h)
  {
   //--- Read current chart dimensions for full-width/height canvas sizing
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   //--- Create the display-resolution sidebar bitmap label canvas
   if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create sidebar canvas"); return false; }
   //--- Create the high-resolution sidebar canvas for supersampled drawing
   if (!m_canvasSidebarHighRes.Create("ToolsPalette_SidebarHR", w * m_supersampleFactor, h * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create sidebar HR canvas"); return false; }
   //--- Create the display-resolution flyout bitmap label canvas
   if (!m_canvasFlyout.CreateBitmapLabel(0, 0, m_nameFlyout, 0, 0, 200, 200, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create flyout canvas"); return false; }
   //--- Create the high-resolution flyout canvas for supersampled drawing
   if (!m_canvasFlyoutHighRes.Create("ToolsPalette_FlyoutHR", 200 * m_supersampleFactor, 200 * m_supersampleFactor, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create flyout HR canvas"); return false; }
   //--- Create the reticle canvas sized to fit the tick-mark geometry
   int reticleSize = 2 * (ReticleOffset + ReticleTickLen / 2) + 6;
   if (!m_canvasReticle.CreateBitmapLabel(0, 0, m_nameReticle, 0, 0, reticleSize, reticleSize, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create reticle canvas"); return false; }
   //--- Hide the reticle until the crosshair tool is active
   ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameReticle, OBJPROP_ZORDER, 90);
   //--- Create the magnifier lens canvas
   if (!m_canvasMagnifier.CreateBitmapLabel(0, 0, m_nameMagnifier, 0, 0, MagDiameter, MagDiameter, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create magnifier canvas"); return false; }
   //--- Hide the magnifier until the crosshair tool is active
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_ZORDER, 95);
   //--- Create the crosshair vertical line canvas (1 pixel wide, full chart height)
   if (!m_canvasCrossVertical.CreateBitmapLabel(0, 0, m_nameCrossVertical, 0, 0, 1, chartH, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create cross vertical canvas"); return false; }
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_ZORDER, 80);
   //--- Pre-fill the vertical line pixels with the chart foreground colour
   DrawCrossVerticalLinePixels(chartH);
   //--- Create the crosshair horizontal line canvas (full chart width, 1 pixel tall)
   if (!m_canvasCrossHorizontal.CreateBitmapLabel(0, 0, m_nameCrossHorizontal, 0, 0, chartW, 1, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create cross horizontal canvas"); return false; }
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_ZORDER, 80);
   //--- Pre-fill the horizontal line pixels with the chart foreground colour
   DrawCrossHorizontalLinePixels(chartW);
   //--- Create the crosshair price axis label canvas
   if (!m_canvasCrossPriceLabel.CreateBitmapLabel(0, 0, m_nameCrossPriceLabel, 0, 0, 80, 18, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create cross price label canvas"); return false; }
   ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_ZORDER, 85);
   //--- Create the crosshair time axis label canvas
   if (!m_canvasCrossTimeLabel.CreateBitmapLabel(0, 0, m_nameCrossTimeLabel, 0, 0, 140, 18, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create cross time label canvas"); return false; }
   ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_ZORDER, 85);
   //--- Create the measure mode vertical anchor line canvas
   if (!m_canvasMeasureVertical.CreateBitmapLabel(0, 0, m_nameMeasureVertical, 0, 0, 1, chartH, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create measure vertical canvas"); return false; }
   ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_ZORDER, 79);
   //--- Pre-fill the measure vertical pixels at reduced opacity
   DrawMeasureVerticalLinePixels(chartH);
   //--- Create the measure mode horizontal anchor line canvas
   if (!m_canvasMeasureHorizontal.CreateBitmapLabel(0, 0, m_nameMeasureHorizontal, 0, 0, chartW, 1, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create measure horizontal canvas"); return false; }
   ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_ZORDER, 79);
   //--- Pre-fill the measure horizontal pixels at reduced opacity
   DrawMeasureHorizontalLinePixels(chartW);
   //--- Create the measure mode price axis label canvas
   if (!m_canvasMeasurePriceLabel.CreateBitmapLabel(0, 0, m_nameMeasurePriceLabel, 0, 0, 80, 18, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create measure price label canvas"); return false; }
   ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMeasurePriceLabel, OBJPROP_ZORDER, 84);
   //--- Create the measure mode time axis label canvas
   if (!m_canvasMeasureTimeLabel.CreateBitmapLabel(0, 0, m_nameMeasureTimeLabel, 0, 0, 140, 18, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create measure time label canvas"); return false; }
   ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMeasureTimeLabel, OBJPROP_ZORDER, 84);
   //--- Create the measure mode diagonal line canvas (full chart size)
   if (!m_canvasMeasureDiagonalLine.CreateBitmapLabel(0, 0, m_nameMeasureDiagonalLine, 0, 0, chartW, chartH, COLOR_FORMAT_ARGB_NORMALIZE))
     { Print("Failed to create measure diagonal canvas"); return false; }
   ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_ZORDER, 78);
   //--- Clear the diagonal canvas to fully transparent and flush
   m_canvasMeasureDiagonalLine.Erase(0x00000000);
   m_canvasMeasureDiagonalLine.Update();
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvas objects and remove chart objects              |
//+------------------------------------------------------------------+
void CCanvasLayer::DestroyAllCanvases()
  {
   //--- ALL canvas objects created with CreateBitmapLabel need explicit ObjectDelete
   //--- after Destroy() — without it they stay as ghost objects on the chart and
   //--- CreateBitmapLabel fails silently on the next Init() call (parameter change restart)
   m_canvasSidebar.Destroy();             ObjectDelete(0, m_nameSidebar);
   m_canvasSidebarHighRes.Destroy();
   m_canvasFlyout.Destroy();              ObjectDelete(0, m_nameFlyout);
   m_canvasFlyoutHighRes.Destroy();
   m_canvasReticle.Destroy();             ObjectDelete(0, m_nameReticle);
   m_canvasMagnifier.Destroy();           ObjectDelete(0, m_nameMagnifier);
   m_canvasCrossVertical.Destroy();       ObjectDelete(0, m_nameCrossVertical);
   m_canvasCrossHorizontal.Destroy();     ObjectDelete(0, m_nameCrossHorizontal);
   m_canvasCrossPriceLabel.Destroy();     ObjectDelete(0, m_nameCrossPriceLabel);
   m_canvasCrossTimeLabel.Destroy();      ObjectDelete(0, m_nameCrossTimeLabel);
   m_canvasMeasureVertical.Destroy();     ObjectDelete(0, m_nameMeasureVertical);
   m_canvasMeasureHorizontal.Destroy();   ObjectDelete(0, m_nameMeasureHorizontal);
   m_canvasMeasurePriceLabel.Destroy();   ObjectDelete(0, m_nameMeasurePriceLabel);
   m_canvasMeasureTimeLabel.Destroy();    ObjectDelete(0, m_nameMeasureTimeLabel);
   m_canvasMeasureDiagonalLine.Destroy(); ObjectDelete(0, m_nameMeasureDiagonalLine);
  }

//+------------------------------------------------------------------+
//| Fill the crosshair vertical line canvas with foreground colour   |
//+------------------------------------------------------------------+
void CCanvasLayer::DrawCrossVerticalLinePixels(int chartH)
  {
   //--- Clear the canvas to fully transparent
   m_canvasCrossVertical.Erase(0x00000000);
   //--- Pack the chart foreground colour at full opacity
   uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255);
   //--- Set every pixel in the single-column canvas to the foreground colour
   for (int y = 0; y < chartH; y++) m_canvasCrossVertical.PixelSet(0, y, col);
   m_canvasCrossVertical.Update();
  }

//+------------------------------------------------------------------+
//| Fill the crosshair horizontal line canvas with foreground colour |
//+------------------------------------------------------------------+
void CCanvasLayer::DrawCrossHorizontalLinePixels(int chartW)
  {
   //--- Clear the canvas to fully transparent
   m_canvasCrossHorizontal.Erase(0x00000000);
   //--- Pack the chart foreground colour at full opacity
   uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 255);
   //--- Set every pixel in the single-row canvas to the foreground colour
   for (int x = 0; x < chartW; x++) m_canvasCrossHorizontal.PixelSet(x, 0, col);
   m_canvasCrossHorizontal.Update();
  }

//+------------------------------------------------------------------+
//| Fill measure vertical line canvas at reduced opacity             |
//+------------------------------------------------------------------+
void CCanvasLayer::DrawMeasureVerticalLinePixels(int chartH)
  {
   //--- Clear the canvas to fully transparent
   m_canvasMeasureVertical.Erase(0x00000000);
   //--- Pack the chart foreground colour at 200/255 opacity for visual distinction
   uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 200);
   //--- Set every pixel in the single-column canvas
   for (int y = 0; y < chartH; y++) m_canvasMeasureVertical.PixelSet(0, y, col);
   m_canvasMeasureVertical.Update();
  }

//+------------------------------------------------------------------+
//| Fill measure horizontal line canvas at reduced opacity           |
//+------------------------------------------------------------------+
void CCanvasLayer::DrawMeasureHorizontalLinePixels(int chartW)
  {
   //--- Clear the canvas to fully transparent
   m_canvasMeasureHorizontal.Erase(0x00000000);
   //--- Pack the chart foreground colour at 200/255 opacity for visual distinction
   uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 200);
   //--- Set every pixel in the single-row canvas
   for (int x = 0; x < chartW; x++) m_canvasMeasureHorizontal.PixelSet(x, 0, col);
   m_canvasMeasureHorizontal.Update();
  }

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

Метод "CreateAllCanvases" теперь считывает размеры графика с помощью ChartGetInteger и создает все пятнадцать холстов последовательно. После существующих холстов боковой панели и выдвижного меню мы создаём холст ретикула, размер которого соответствует геометрии делений, заданной входными параметрами, холст лупы, размер которого определяется диаметром лупы, а также вертикальный и горизонтальный холст перекрестия размером один пиксель на всю высоту графика и один пиксель на всю ширину графика соответственно. Сразу после создания мы предварительно заполняем оба холста с линиями перекрестия цветом переднего плана графика. Затем создаем холсты с метками для осей цены и времени, после чего следуют эквиваленты в режиме измерения, где холсты вертикальных и горизонтальных линий предварительно заполняются с пониженной прозрачностью для визуального отличия от текущего перекрестия. Наконец, создаем диагональный холст на весь экран и очищаем его до прозрачного состояния. Каждому холсту присваивается параметр z-порядка для обеспечения корректного наложения слоев, и он изначально скрыт с параметром OBJ_NO_PERIODS до активации инструмента перекрестия.

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

Вспомогательные методы для четырех линий пикселей следуют тому же шаблону. "DrawCrossVerticalLinePixels" и "DrawCrossHorizontalLinePixels" очищают холст, задают цвет переднего плана графика с полной непрозрачностью, используя ColorToARGB, и заполняют каждый пиксель в одноколоночном или однострочном холсте. "DrawMeasureVerticalLinePixels" и "DrawMeasureHorizontalLinePixels" делают то же самое, но с непрозрачностью 200 из 255, придавая линиям привязки измерения слегка приглушённый вид, визуально отделяющий их от движущихся линий перекрестия. Для управления линиями перекрестия мы вводим специальный класс, который централизует логику для упрощения будущих изменений. Поскольку это новый класс, давайте сначала определим его и его члены.

Объявление класса менеджера перекрестий

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

//+------------------------------------------------------------------+
//| CLASS 8 — Manage crosshair, reticle, magnifier, and measure mode |
//+------------------------------------------------------------------+
class CCrosshairManager : public CSidebarRenderer
  {
protected:
   int      m_reticleCanvasSize;            // Pixel size of the square reticle canvas
   bool     m_isReticleVisible;             // Flag indicating the reticle is currently shown
   bool     m_isMagnifierVisible;           // Flag indicating the magnifier lens is shown
   bool     m_isCrossVertVisible;           // Flag indicating the vertical crosshair line is shown
   bool     m_isCrossHorizVisible;          // Flag indicating the horizontal crosshair line is shown
   bool     m_isCrossPriceLabelVisible;     // Flag indicating the crosshair price label is shown
   bool     m_isCrossTimeLabelVisible;      // Flag indicating the crosshair time label is shown
   bool     m_isMeasureVertVisible;         // Flag indicating the measure vertical anchor line is shown
   bool     m_isMeasureHorizVisible;        // Flag indicating the measure horizontal anchor line is shown
   bool     m_isMeasurePriceLabelVisible;   // Flag indicating the measure price label is shown
   bool     m_isMeasureTimeLabelVisible;    // Flag indicating the measure time label is shown
   bool     m_isMeasureDiagonalVisible;     // Flag indicating the measure diagonal line is shown
   bool     m_isMeasuringActive;            // Flag indicating measure mode is locked to an anchor point
   datetime m_measureAnchorTime;            // Chart time of the measure mode anchor point
   double   m_measureAnchorPrice;           // Price level of the measure mode anchor point
   int      m_measureAnchorPixelX;          // Screen X of the measure mode anchor point
   int      m_measureAnchorPixelY;          // Screen Y of the measure mode anchor point
   ulong    m_lastClickTimeMicros;          // Microsecond timestamp of the last mouse click
   int      m_lastMagMouseX;                // Last mouse X used to draw the magnifier lens
   int      m_lastMagMouseY;                // Last mouse Y used to draw the magnifier lens

protected:
   //--- Draw the reticle tick-mark crosses onto the reticle canvas
   void DrawReticleTickMarks();
   //--- Make the reticle canvas visible on the chart
   void ShowReticle();
   //--- Hide the reticle canvas from the chart
   void HideReticle();
   //--- Move the reticle canvas to follow the mouse cursor
   void UpdateReticlePosition(int mouseX, int mouseY);
   //--- Make the crosshair vertical line canvas visible
   void ShowCrossVertical();
   //--- Hide the crosshair vertical line canvas
   void HideCrossVertical();
   //--- Move the crosshair vertical line to the given screen X
   void UpdateCrossVerticalPosition(int mouseX);
   //--- Make the crosshair horizontal line canvas visible
   void ShowCrossHorizontal();
   //--- Hide the crosshair horizontal line canvas
   void HideCrossHorizontal();
   //--- Move the crosshair horizontal line to the given screen Y
   void UpdateCrossHorizontalPosition(int mouseY);
   //--- Make the crosshair price axis label visible
   void ShowCrossPriceLabel();
   //--- Hide the crosshair price axis label
   void HideCrossPriceLabel();
   //--- Make the crosshair time axis label visible
   void ShowCrossTimeLabel();
   //--- Hide the crosshair time axis label
   void HideCrossTimeLabel();
   //--- Draw and position one axis label canvas next to the crosshair
   void DrawAndPositionAxisLabel(CCanvas &labelCanvas, string objectName, string labelText,
                                  bool isPriceAxis, int crosshairPixelPos, int chartWidth, int chartHeight);
   //--- Update both crosshair axis labels to reflect the current mouse position
   void UpdateCrosshairAxisLabels(int mouseX, int mouseY, datetime barTime, double barPrice);
   //--- Show all measure mode line and label canvases
   void ShowMeasureLines();
   //--- Hide all measure mode line and label canvases
   void HideMeasureLines();
   //--- Move the measure vertical anchor line to the given screen X
   void UpdateMeasureVerticalPosition(int pixelX);
   //--- Move the measure horizontal anchor line to the given screen Y
   void UpdateMeasureHorizontalPosition(int pixelY);
   //--- Update the measure anchor axis labels at the anchor chart coordinate
   void UpdateMeasureAnchorLabels();
   //--- Make the magnifier lens canvas visible
   void ShowMagnifier();
   //--- Hide the magnifier lens canvas
   void HideMagnifier();
   //--- Move the magnifier and redraw its lens content if the mouse has moved
   void UpdateMagnifierPosition(int mouseX, int mouseY, datetime barTime, double barPrice);
   //--- Render the zoomed candle chart content inside the circular magnifier lens
   void DrawMagnifierLensContent(int mouseX, int mouseY, datetime centerTime, double centerPrice);
   //--- Redraw the measure diagonal line from the anchor to the current mouse position
   void UpdateMeasureDiagonalLine(int currentMouseX, int currentMouseY);
   //--- Update the floating measure info label near the cursor with bar/pip statistics
   void UpdateMeasurementInfoLabel(int mouseX, int mouseY, datetime barTime, double barPrice);
   //--- Hide all crosshair element canvases in one call
   void HideAllCrosshairElements();
   //--- Show all crosshair element canvases in one call
   void ShowAllCrosshairElements();
   //--- Handle a potential double-click to toggle measure mode anchor
   void HandleCrosshairDoubleClick(int mouseX, int mouseY, datetime barTime, double barPrice);
   //--- Delete all measure mode chart objects and hide canvases
   void DeleteAllMeasureObjects();
  };

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

Класс объявляет тридцать защищенных методов, организованных в четыре группы. Группа методов, связанных с ретикулом, охватывает рисование делений, отображение, скрытие и изменение положения холста сетки. Группа линий перекрестия обрабатывает отображение, скрытие и позиционирование вертикальных и горизонтальных линий, а также отображение, скрытие, рисование и позиционирование меток оси цены и времени с помощью общего метода "DrawAndPositionAxisLabel". Группа режимов измерения управляет отображением и скрытием всех холстов для измерения, позиционированием якорных линий, обновлением меток якорных меток, рисованием диагональной линии и обновлением плавающей информационной метки с указанием количества баров и статистики по пипсам. Группа лупы отвечает за отображение, скрытие, позиционирование и рендеринг увеличенного содержимого свечи внутри круглой линзы. Наконец, два вспомогательных метода показывают или скрывают все элементы перекрестия за один вызов. Метод "HandleCrosshairDoubleClick" переключает привязку измерения при двойном щелчке, а "DeleteAllMeasureObjects" очищает все ресурсы режима измерения. Давайте теперь определим все эти методы.

//+------------------------------------------------------------------+
//| Draw the reticle tick-mark crosses onto the reticle canvas       |
//+------------------------------------------------------------------+
void CCrosshairManager::DrawReticleTickMarks()
  {
   //--- Clear the reticle canvas to fully transparent
   m_canvasReticle.Erase(0x00000000);
   //--- Compute the centre of the square reticle canvas
   int cx = m_reticleCanvasSize / 2, cy = m_reticleCanvasSize / 2;
   //--- Cache tick geometry from inputs
   int off = ReticleOffset, tl = ReticleTickLen / 2, th = ReticleThickness;
   //--- Pack the chart foreground colour at slightly reduced opacity
   uint col = ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 230);
   //--- Draw left tick marks above and below the horizontal axis
   m_canvasReticle.FillRectangle(cx - off - tl, cy - th - 1, cx - off + tl, cy - 2,      col);
   m_canvasReticle.FillRectangle(cx - off - tl, cy + 2,      cx - off + tl, cy + th + 1, col);
   //--- Draw right tick marks above and below the horizontal axis
   m_canvasReticle.FillRectangle(cx + off - tl, cy - th - 1, cx + off + tl, cy - 2,      col);
   m_canvasReticle.FillRectangle(cx + off - tl, cy + 2,      cx + off + tl, cy + th + 1, col);
   //--- Draw top tick marks left and right of the vertical axis
   m_canvasReticle.FillRectangle(cx - th - 1, cy - off - tl, cx - 2,      cy - off + tl, col);
   m_canvasReticle.FillRectangle(cx + 2,      cy - off - tl, cx + th + 1, cy - off + tl, col);
   //--- Draw bottom tick marks left and right of the vertical axis
   m_canvasReticle.FillRectangle(cx - th - 1, cy + off - tl, cx - 2,      cy + off + tl, col);
   m_canvasReticle.FillRectangle(cx + 2,      cy + off - tl, cx + th + 1, cy + off + tl, col);
   m_canvasReticle.Update();
  }

//+------------------------------------------------------------------+
//| Make the reticle canvas visible on the chart                     |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowReticle()
  {
   //--- Skip if already visible
   if (m_isReticleVisible) return;
   //--- Draw fresh tick marks before making visible
   DrawReticleTickMarks();
   //--- Make the reticle chart object visible on all timeframes
   ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isReticleVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the reticle canvas from the chart                           |
//+------------------------------------------------------------------+
void CCrosshairManager::HideReticle()
  {
   //--- Skip if already hidden
   if (!m_isReticleVisible) return;
   ObjectSetInteger(0, m_nameReticle, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isReticleVisible = false;
  }

//+------------------------------------------------------------------+
//| Move the reticle canvas to follow the mouse cursor               |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateReticlePosition(int mouseX, int mouseY)
  {
   if (!m_isReticleVisible) return;
   //--- Centre the reticle canvas on the mouse cursor
   int half = m_reticleCanvasSize / 2;
   ObjectSetInteger(0, m_nameReticle, OBJPROP_XDISTANCE, mouseX - half);
   ObjectSetInteger(0, m_nameReticle, OBJPROP_YDISTANCE, mouseY - half);
  }

//+------------------------------------------------------------------+
//| Make the crosshair vertical line canvas visible                  |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowCrossVertical()
  {
   if (m_isCrossVertVisible) return;
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isCrossVertVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the crosshair vertical line canvas                          |
//+------------------------------------------------------------------+
void CCrosshairManager::HideCrossVertical()
  {
   if (!m_isCrossVertVisible) return;
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isCrossVertVisible = false;
  }

//+------------------------------------------------------------------+
//| Move the crosshair vertical line to the given screen X           |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateCrossVerticalPosition(int mouseX)
  {
   if (!m_isCrossVertVisible) return;
   //--- Position the 1-pixel-wide canvas at the cursor X
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_XDISTANCE, mouseX);
   ObjectSetInteger(0, m_nameCrossVertical, OBJPROP_YDISTANCE, 0);
  }

//+------------------------------------------------------------------+
//| Make the crosshair horizontal line canvas visible                |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowCrossHorizontal()
  {
   if (m_isCrossHorizVisible) return;
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isCrossHorizVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the crosshair horizontal line canvas                        |
//+------------------------------------------------------------------+
void CCrosshairManager::HideCrossHorizontal()
  {
   if (!m_isCrossHorizVisible) return;
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isCrossHorizVisible = false;
  }

//+------------------------------------------------------------------+
//| Move the crosshair horizontal line to the given screen Y         |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateCrossHorizontalPosition(int mouseY)
  {
   if (!m_isCrossHorizVisible) return;
   //--- Position the 1-pixel-tall canvas at the cursor Y
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_XDISTANCE, 0);
   ObjectSetInteger(0, m_nameCrossHorizontal, OBJPROP_YDISTANCE, mouseY);
  }

//+------------------------------------------------------------------+
//| Make the crosshair price axis label visible                      |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowCrossPriceLabel()
  {
   if (m_isCrossPriceLabelVisible) return;
   ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isCrossPriceLabelVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the crosshair price axis label                              |
//+------------------------------------------------------------------+
void CCrosshairManager::HideCrossPriceLabel()
  {
   if (!m_isCrossPriceLabelVisible) return;
   ObjectSetInteger(0, m_nameCrossPriceLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isCrossPriceLabelVisible = false;
  }

//+------------------------------------------------------------------+
//| Make the crosshair time axis label visible                       |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowCrossTimeLabel()
  {
   if (m_isCrossTimeLabelVisible) return;
   ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isCrossTimeLabelVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the crosshair time axis label                               |
//+------------------------------------------------------------------+
void CCrosshairManager::HideCrossTimeLabel()
  {
   if (!m_isCrossTimeLabelVisible) return;
   ObjectSetInteger(0, m_nameCrossTimeLabel, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isCrossTimeLabelVisible = false;
  }

//+------------------------------------------------------------------+
//| Draw and position one axis label canvas next to the crosshair   |
//+------------------------------------------------------------------+
void CCrosshairManager::DrawAndPositionAxisLabel(CCanvas &labelCanvas, string objectName,
                                                  string labelText, bool isPriceAxis,
                                                  int crosshairPixelPos, int chartWidth, int chartHeight)
  {
   color fgColor = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND);
   color bgColor = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   uint  fg = ColorToARGB(fgColor, 255), bg = ColorToARGB(bgColor, 255);
   //--- Measure text using standalone API
   TextSetFont(AxisLabelFont, -AxisLabelFontSize * 10);
   uint tw = 0, th = 0;
   TextGetSize(labelText, tw, th);
   int lw = (int)tw + 8, lh = (int)th + 4;
   if (labelCanvas.Width() != lw || labelCanvas.Height() != lh) labelCanvas.Resize(lw, lh);
   ObjectSetInteger(0, objectName, OBJPROP_XSIZE, lw);
   ObjectSetInteger(0, objectName, OBJPROP_YSIZE, lh);
   //--- Render text into raw buffer using XRGB — no alpha, pure colors
   uint textBuf[];
   int totalPx = lw * lh;
   ArrayResize(textBuf, totalPx);
   ArrayFill(textBuf, 0, totalPx, bg & 0x00FFFFFF);
   TextOut(labelText, 4, 2, TA_LEFT | TA_TOP, textBuf, lw, lh, fg & 0x00FFFFFF, COLOR_FORMAT_XRGB_NOALPHA);
   //--- Copy clean pixels onto canvas with full alpha
   for (int py = 0; py < lh; py++)
      for (int px = 0; px < lw; px++)
         labelCanvas.PixelSet(px, py, textBuf[py * lw + px] | 0xFF000000);
   //--- Draw border on top
   labelCanvas.Rectangle(0, 0, lw - 1, lh - 1, fg);
   labelCanvas.Update();
   //--- Position the label at the appropriate axis edge
   if (isPriceAxis)
     {
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, chartWidth - lw + 1);
      ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, crosshairPixelPos - lh / 2);
     }
   else
     {
      ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, crosshairPixelPos - lw / 2);
      ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, chartHeight - lh);
     }
  }
//+------------------------------------------------------------------+
//| Update both crosshair axis labels for the current mouse position |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateCrosshairAxisLabels(int mouseX, int mouseY, datetime barTime, double barPrice)
  {
   //--- Read chart dimensions and symbol digit count
   int chartW  = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartH  = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int digits  = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   //--- Draw and position the price label on the right axis at cursor Y
   DrawAndPositionAxisLabel(m_canvasCrossPriceLabel, m_nameCrossPriceLabel,
      DoubleToString(barPrice, digits), true, mouseY, chartW, chartH);
   //--- Draw and position the time label on the bottom axis at cursor X
   DrawAndPositionAxisLabel(m_canvasCrossTimeLabel, m_nameCrossTimeLabel,
      TimeToString(barTime, TIME_DATE | TIME_MINUTES), false, mouseX, chartW, chartH);
  }

//+------------------------------------------------------------------+
//| Show all measure mode line and label canvases                    |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowMeasureLines()
  {
   //--- Show each measure canvas if not already visible
   if (!m_isMeasureVertVisible)
     { ObjectSetInteger(0, m_nameMeasureVertical,     OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureVertVisible     = true; }
   if (!m_isMeasureHorizVisible)
     { ObjectSetInteger(0, m_nameMeasureHorizontal,   OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureHorizVisible    = true; }
   if (!m_isMeasureDiagonalVisible)
     { ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureDiagonalVisible = true; }
   if (!m_isMeasurePriceLabelVisible)
     { ObjectSetInteger(0, m_nameMeasurePriceLabel,   OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasurePriceLabelVisible = true; }
   if (!m_isMeasureTimeLabelVisible)
     { ObjectSetInteger(0, m_nameMeasureTimeLabel,    OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); m_isMeasureTimeLabelVisible  = true; }
  }

//+------------------------------------------------------------------+
//| Hide all measure mode line and label canvases                    |
//+------------------------------------------------------------------+
void CCrosshairManager::HideMeasureLines()
  {
   //--- Hide each measure canvas if currently visible
   if (m_isMeasureVertVisible)
     { ObjectSetInteger(0, m_nameMeasureVertical,     OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureVertVisible     = false; }
   if (m_isMeasureHorizVisible)
     { ObjectSetInteger(0, m_nameMeasureHorizontal,   OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureHorizVisible    = false; }
   if (m_isMeasureDiagonalVisible)
     {
      //--- Clear the diagonal canvas before hiding to avoid stale pixels
      ObjectSetInteger(0, m_nameMeasureDiagonalLine, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
      m_canvasMeasureDiagonalLine.Erase(0x00000000);
      m_canvasMeasureDiagonalLine.Update();
      m_isMeasureDiagonalVisible = false;
     }
   if (m_isMeasurePriceLabelVisible)
     { ObjectSetInteger(0, m_nameMeasurePriceLabel,   OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasurePriceLabelVisible = false; }
   if (m_isMeasureTimeLabelVisible)
     { ObjectSetInteger(0, m_nameMeasureTimeLabel,    OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); m_isMeasureTimeLabelVisible  = false; }
  }

//+------------------------------------------------------------------+
//| Move the measure vertical anchor line to the given screen X      |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMeasureVerticalPosition(int pixelX)
  {
   if (!m_isMeasureVertVisible) return;
   ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_XDISTANCE, pixelX);
   ObjectSetInteger(0, m_nameMeasureVertical, OBJPROP_YDISTANCE, 0);
  }

//+------------------------------------------------------------------+
//| Move the measure horizontal anchor line to the given screen Y    |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMeasureHorizontalPosition(int pixelY)
  {
   if (!m_isMeasureHorizVisible) return;
   ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_XDISTANCE, 0);
   ObjectSetInteger(0, m_nameMeasureHorizontal, OBJPROP_YDISTANCE, pixelY);
  }

//+------------------------------------------------------------------+
//| Update the measure anchor axis labels at the anchor coordinate   |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMeasureAnchorLabels()
  {
   //--- Read chart dimensions and symbol digit count
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   //--- Convert the anchor chart coordinate to screen pixel position
   int fx = 0, fy = 0;
   if (!ChartTimePriceToXY(m_chartId, 0, m_measureAnchorTime, m_measureAnchorPrice, fx, fy)) return;
   //--- Draw and position the anchor price label on the right axis
   DrawAndPositionAxisLabel(m_canvasMeasurePriceLabel, m_nameMeasurePriceLabel,
      DoubleToString(m_measureAnchorPrice, digits), true, fy, chartW, chartH);
   //--- Draw and position the anchor time label on the bottom axis
   DrawAndPositionAxisLabel(m_canvasMeasureTimeLabel, m_nameMeasureTimeLabel,
      TimeToString(m_measureAnchorTime, TIME_DATE | TIME_MINUTES), false, fx, chartW, chartH);
  }

//+------------------------------------------------------------------+
//| Make the magnifier lens canvas visible                           |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowMagnifier()
  {
   if (m_isMagnifierVisible) return;
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS);
   m_isMagnifierVisible = true;
  }

//+------------------------------------------------------------------+
//| Hide the magnifier lens canvas                                   |
//+------------------------------------------------------------------+
void CCrosshairManager::HideMagnifier()
  {
   if (!m_isMagnifierVisible) return;
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS);
   m_isMagnifierVisible = false;
  }

//+------------------------------------------------------------------+
//| Move the magnifier and redraw lens content if cursor has moved   |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMagnifierPosition(int mouseX, int mouseY, datetime barTime, double barPrice)
  {
   if (!m_isMagnifierVisible) return;
   int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int diam   = MagDiameter;
   //--- Prefer placing the magnifier to the upper-right of the cursor
   int magX = (mouseX + MagOffset + diam < chartW) ? mouseX + MagOffset : mouseX - MagOffset - diam;
   int magY = (mouseY - MagOffset - diam > 0)      ? mouseY - MagOffset - diam : mouseY + MagOffset;
   //--- Clamp the magnifier position within chart bounds
   magX = MathMax(2, MathMin(chartW - diam - 2, magX));
   magY = MathMax(2, MathMin(chartH - diam - 2, magY));
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_XDISTANCE, magX);
   ObjectSetInteger(0, m_nameMagnifier, OBJPROP_YDISTANCE, magY);
   //--- Skip redrawing lens content if cursor has not moved
   if (mouseX == m_lastMagMouseX && mouseY == m_lastMagMouseY) return;
   m_lastMagMouseX = mouseX;
   m_lastMagMouseY = mouseY;
   //--- Redraw lens content for the new cursor position
   DrawMagnifierLensContent(mouseX, mouseY, barTime, barPrice);
  }

//+------------------------------------------------------------------+
//| Render zoomed candle chart content inside the magnifier lens     |
//+------------------------------------------------------------------+
void CCrosshairManager::DrawMagnifierLensContent(int mouseX, int mouseY, datetime centerTime, double centerPrice)
  {
   int    diam   = MagDiameter, radius = diam / 2;
   double zoom   = MagZoom;
   //--- Resize magnifier canvas if diameter has changed
   if (m_canvasMagnifier.Width() != diam || m_canvasMagnifier.Height() != diam)
      m_canvasMagnifier.Resize(diam, diam);
   m_canvasMagnifier.Erase(0x00000000);
   //--- Read chart colour settings for consistent lens rendering
   color bgColor  = (color)ChartGetInteger(0, CHART_COLOR_BACKGROUND);
   color fgColor  = (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND);
   color bullBody = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL);
   color bearBody = (color)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR);
   color bullBord = (color)ChartGetInteger(0, CHART_COLOR_CHART_UP);
   color bearBord = (color)ChartGetInteger(0, CHART_COLOR_CHART_DOWN);
   color askColor = (color)ChartGetInteger(0, CHART_COLOR_ASK);
   color bidColor = (color)ChartGetInteger(0, CHART_COLOR_BID);
   bool  showAsk  = (ChartGetInteger(0, CHART_SHOW_ASK_LINE) != 0);
   bool  showBid  = (ChartGetInteger(0, CHART_SHOW_BID_LINE) != 0);
   //--- Read chart price range and bar width for coordinate mapping
   int    chartH      = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   double chartMax    = ChartGetDouble(0, CHART_PRICE_MAX, 0);
   double chartMin    = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   double chartRange  = MathMax(chartMax - chartMin, _Point * 100);
   int    barWidth    = (int)MathPow(2.0, (int)ChartGetInteger(0, CHART_SCALE));
   double pricePerPixel = chartRange / chartH;
   double radiusSq    = (double)(radius - 3) * (radius - 3);
   //--- Fill the circular lens background
   uint   bgARGB  = ColorToARGB(bgColor, 255);
   double bgRSq   = (double)(radius - 2) * (radius - 2);
   for (int py = 0; py < diam; py++)
      for (int px = 0; px < diam; px++)
        {
         double ddx = px - radius, ddy = py - radius;
         if (ddx * ddx + ddy * ddy <= bgRSq) m_canvasMagnifier.PixelSet(px, py, bgARGB);
        }
   //--- Compute bar range to fetch based on zoom and visible bar width
   int chartVisibleBars = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   int halfRange = MathMin((int)((radius * 2.0 / zoom) / MathMax(1, barWidth)) / 2 + 2, chartVisibleBars / 2 + 2);
   int cursorBar = iBarShift(_Symbol, _Period, centerTime, false);
   if (cursorBar < 0) cursorBar = 0;
   //--- Fetch OHLC rates for the visible bar range
   MqlRates rates[];
   ArraySetAsSeries(rates, false);
   int startBar = MathMax(0, cursorBar - halfRange);
   int copied   = CopyRates(_Symbol, _Period, startBar, cursorBar + halfRange + 1 - startBar, rates);
   //--- Compute zoomed wick and border thickness
   int wickThickness   = MathMax(1, (int)MathRound(zoom * 0.55));
   int borderThickness = MathMax(1, (int)MathRound(zoom * 0.45));
   //--- Draw each candle inside the lens clipped to the circle
   if (copied > 0)
     {
      for (int i = 0; i < copied; i++)
        {
         //--- Convert bar time/price to screen pixel position
         int barPxX = 0, barPxY = 0;
         if (!ChartTimePriceToXY(m_chartId, 0, rates[i].time, rates[i].close, barPxX, barPxY)) continue;
         //--- Map screen pixel to lens X using zoom factor
         int lensX = radius + (int)((barPxX - mouseX) * zoom);
         //--- Compute zoomed bar body width
         int zbw = MathMax(3, (int)(barWidth * zoom * 0.65)); if (zbw % 2 == 0) zbw++;
         int bh  = zbw / 2;
         if (lensX + bh < 0 || lensX - bh >= diam) continue;
         //--- Determine candle direction and pack colours
         bool isBull     = (rates[i].close >= rates[i].open);
         uint wickARGB   = ColorToARGB(isBull ? bullBord : bearBord, 255);
         uint bodyARGB   = ColorToARGB(isBull ? bullBody : bearBody, 255);
         uint borderARGB = ColorToARGB(isBull ? bullBord : bearBord, 255);
         //--- Convert high/low/opru/close to lens Y coordinates
         int lensHi = radius - (int)((rates[i].high  - centerPrice) / pricePerPixel * zoom);
         int lensLo = radius - (int)((rates[i].low   - centerPrice) / pricePerPixel * zoom);
         double bTop = isBull ? rates[i].close : rates[i].open;
         double bBot = isBull ? rates[i].open  : rates[i].close;
         int lensBT  = radius - (int)((bTop - centerPrice) / pricePerPixel * zoom);
         int lensBB  = radius - (int)((bBot - centerPrice) / pricePerPixel * zoom);
         //--- Ensure minimum body height of 1 pixel
         if (lensBB - lensBT < 1) lensBB = lensBT + 1;
         //--- Draw the wick clipped to the lens circle
         int wickHalf = wickThickness / 2;
         for (int wy = MathMax(0, lensHi); wy <= MathMin(diam - 1, lensLo); wy++)
            for (int wx = lensX - wickHalf; wx <= lensX + wickHalf; wx++)
              {
               if (wx < 0 || wx >= diam) continue;
               double ddx = wx - radius, ddy = wy - radius;
               if (ddx * ddx + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(wx, wy, wickARGB);
              }
         //--- Draw the candle body fill clipped to the lens circle
         for (int by = MathMax(0, lensBT); by <= MathMin(diam - 1, lensBB); by++)
            for (int bx = lensX - bh; bx <= lensX + bh; bx++)
              {
               if (bx < 0 || bx >= diam) continue;
               double ddx = bx - radius, ddy = by - radius;
               if (ddx * ddx + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(bx, by, bodyARGB);
              }
         //--- Draw the candle body border clipped to the lens circle
         for (int bt = 0; bt < borderThickness; bt++)
           {
            int topRow = MathMax(0, lensBT + bt), botRow = MathMin(diam - 1, lensBB - bt);
            for (int bx = lensX - bh; bx <= lensX + bh; bx++)
              {
               if (bx < 0 || bx >= diam) continue;
               double ddx = bx - radius;
               double ddyT = topRow - radius, ddyB = botRow - radius;
               if (ddx * ddx + ddyT * ddyT < radiusSq) m_canvasMagnifier.PixelSet(bx, topRow, borderARGB);
               if (ddx * ddx + ddyB * ddyB < radiusSq) m_canvasMagnifier.PixelSet(bx, botRow, borderARGB);
              }
            int leftCol = lensX - bh + bt, rightCol = lensX + bh - bt;
            for (int by = MathMax(0, lensBT); by <= MathMin(diam - 1, lensBB); by++)
              {
               double ddy  = by - radius;
               double ddxL = leftCol  - radius, ddxR = rightCol - radius;
               if (leftCol  >= 0 && leftCol  < diam && ddxL * ddxL + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(leftCol,  by, borderARGB);
               if (rightCol >= 0 && rightCol < diam && ddxR * ddxR + ddy * ddy < radiusSq) m_canvasMagnifier.PixelSet(rightCol, by, borderARGB);
              }
           }
        }
     }
   //--- Draw Bid line inside lens if enabled
   if (showBid)
     {
      double bidPrice = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      int    bidY     = radius - (int)((bidPrice - centerPrice) / pricePerPixel * zoom);
      uint   bidARGB  = ColorToARGB(bidColor, 200);
      for (int gx = 0; gx < diam; gx++)
        {
         double ddx = gx - radius, ddy = bidY - radius;
         if (ddx * ddx + ddy * ddy < radiusSq) BlendPixelSet(m_canvasMagnifier, gx, bidY, bidARGB);
        }
     }
   //--- Draw Ask line inside lens if enabled
   if (showAsk)
     {
      double askPrice = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      int    askY     = radius - (int)((askPrice - centerPrice) / pricePerPixel * zoom);
      uint   askARGB  = ColorToARGB(askColor, 200);
      for (int gx = 0; gx < diam; gx++)
        {
         double ddx = gx - radius, ddy = askY - radius;
         if (ddx * ddx + ddy * ddy < radiusSq) BlendPixelSet(m_canvasMagnifier, gx, askY, askARGB);
        }
     }
   //--- Draw the anti-aliased circular lens ring border
   uint   ringARGB = ColorToARGB(m_isDarkTheme ? C'140,150,170' : C'80,90,110', 255);
   double outerR   = radius - 1.0, innerR = outerR - 2.5;
   for (int py = 0; py < diam; py++)
      for (int px = 0; px < diam; px++)
        {
         double ddx = px - radius + 0.5, ddy = py - radius + 0.5;
         double dist = MathSqrt(ddx * ddx + ddy * ddy);
         if (dist < innerR - 1.0 || dist > outerR + 1.0) continue;
         //--- Compute anti-aliased ring edge coverage
         double alpha = MathMin(MathMin(1.0, dist - (innerR - 1.0)), MathMin(1.0, outerR + 1.0 - dist));
         if (alpha <= 0.0) continue;
         BlendPixelSet(m_canvasMagnifier, px, py, ((uint)(uchar)(alpha * 255.0) << 24) | (ringARGB & 0x00FFFFFF));
        }
   //--- Draw a faint dashed crosshair at lens centre
   uint crossARGB = ColorToARGB(fgColor, 60);
   for (int px = 0; px < diam; px++)
     {
      if (px % 4 == 0) continue;
      double ddx = (double)(px - radius);
      if (ddx * ddx >= radiusSq) continue;
      BlendPixelSet(m_canvasMagnifier, px, radius, crossARGB);
     }
   for (int py = 0; py < diam; py++)
     {
      if (py % 4 == 0) continue;
      double ddy = (double)(py - radius);
      if (ddy * ddy >= radiusSq) continue;
      BlendPixelSet(m_canvasMagnifier, radius, py, crossARGB);
     }
   //--- Draw the current price label inside the lens near the bottom
   int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   m_canvasMagnifier.FontSet("Arial Bold", 11);
   string priceStr = DoubleToString(centerPrice, digits);
   int tw = m_canvasMagnifier.TextWidth(priceStr), th = m_canvasMagnifier.TextHeight(priceStr);
   int tx = radius - tw / 2, ty = diam - th - 16;
   double tdy = ty - radius;
   //--- Only draw the label if it fits within the lens circle
   if (tdy * tdy + 4 < radiusSq)
     {
      m_canvasMagnifier.FillRectangle(tx - 4, ty - 1, tx + tw + 4, ty + th + 1,
         (ringARGB & 0x00FFFFFF) | 0xFF000000);
      m_canvasMagnifier.TextOut(tx, ty, priceStr, ColorToARGB(clrWhite, 255));
     }
   m_canvasMagnifier.Update();
  }

//+------------------------------------------------------------------+
//| Redraw the measure diagonal line from anchor to current cursor   |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMeasureDiagonalLine(int currentMouseX, int currentMouseY)
  {
   if (!m_isMeasureDiagonalVisible) return;
   //--- Clear the full-screen diagonal canvas before redrawing
   m_canvasMeasureDiagonalLine.Erase(0x00000000);
   //--- Draw an anti-aliased Bresenham line from anchor to current cursor
   DrawBresenhamLine(m_canvasMeasureDiagonalLine,
      m_measureAnchorPixelX, m_measureAnchorPixelY, currentMouseX, currentMouseY,
      ColorToARGB((color)ChartGetInteger(0, CHART_COLOR_FOREGROUND), 220));
   m_canvasMeasureDiagonalLine.Update();
  }

//+------------------------------------------------------------------+
//| Update the floating measure info label near the cursor           |
//+------------------------------------------------------------------+
void CCrosshairManager::UpdateMeasurementInfoLabel(int mouseX, int mouseY, datetime barTime, double barPrice)
  {
   string labelName = "ToolsPalette_MeasureInfoLabel";
   //--- Compute bar count between anchor and cursor using period seconds
   long   periodSec = PeriodSeconds(_Period);
   int    barCount  = (int)MathAbs(m_measureAnchorTime / periodSec - barTime / periodSec);
   //--- Compute pip distance using correct pip size for the symbol
   double pointSize = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   long   digits    = SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   double pipSize   = (digits == 3 || digits == 5) ? pointSize * 10.0 : pointSize;
   double pips      = MathAbs(barPrice - m_measureAnchorPrice) / pipSize;
   //--- Build the label text string with bar count, pip distance, and raw price difference
   string labelText = StringFormat("%d bars, %.1f pips, Diff: %s",
      barCount, pips, DoubleToString(MathAbs(barPrice - m_measureAnchorPrice), (int)digits));
   //--- Create the OBJ_LABEL chart object if it does not yet exist
   if (ObjectFind(m_chartId, labelName) < 0)
     {
      ObjectCreate(m_chartId, labelName, OBJ_LABEL, 0, 0, 0);
      ObjectSetInteger(m_chartId, labelName, OBJPROP_CORNER,   CORNER_LEFT_UPPER);
      ObjectSetInteger(m_chartId, labelName, OBJPROP_FONTSIZE, 9);
      ObjectSetString(m_chartId,  labelName, OBJPROP_FONT,     "Arial");
      ObjectSetInteger(m_chartId, labelName, OBJPROP_COLOR,
         (color)ChartGetInteger(0, CHART_COLOR_FOREGROUND));
     }
   //--- Update the label position to follow the cursor with a small offset
   ObjectSetInteger(m_chartId, labelName, OBJPROP_XDISTANCE, mouseX + 20);
   ObjectSetInteger(m_chartId, labelName, OBJPROP_YDISTANCE, mouseY + 3);
   ObjectSetString(m_chartId,  labelName, OBJPROP_TEXT,      labelText);
  }

//+------------------------------------------------------------------+
//| Hide all crosshair element canvases in one call                  |
//+------------------------------------------------------------------+
void CCrosshairManager::HideAllCrosshairElements()
  {
   HideReticle();
   HideMagnifier();
   HideCrossVertical();
   HideCrossHorizontal();
   HideCrossPriceLabel();
   HideCrossTimeLabel();
  }

//+------------------------------------------------------------------+
//| Show all crosshair element canvases in one call                  |
//+------------------------------------------------------------------+
void CCrosshairManager::ShowAllCrosshairElements()
  {
   ShowReticle();
   ShowMagnifier();
   ShowCrossVertical();
   ShowCrossHorizontal();
   ShowCrossPriceLabel();
   ShowCrossTimeLabel();
  }

//+------------------------------------------------------------------+
//| Delete all measure mode chart objects and hide canvases          |
//+------------------------------------------------------------------+
void CCrosshairManager::DeleteAllMeasureObjects()
  {
   //--- Hide all measure canvases
   HideMeasureLines();
   //--- Remove the floating info label chart object
   ObjectDelete(m_chartId, "ToolsPalette_MeasureInfoLabel");
  }

//+------------------------------------------------------------------+
//| Handle a potential double-click to toggle measure mode anchor    |
//+------------------------------------------------------------------+
void CCrosshairManager::HandleCrosshairDoubleClick(int mouseX, int mouseY, datetime barTime, double barPrice)
  {
   ulong nowMicros = GetMicrosecondCount();
   //--- Detect double-click by checking time since last click
   if (nowMicros - m_lastClickTimeMicros < 500000)
     {
      if (!m_isMeasuringActive)
        {
         //--- Lock the measure anchor to the current chart coordinate
         m_measureAnchorTime   = barTime;
         m_measureAnchorPrice  = barPrice;
         m_measureAnchorPixelX = mouseX;
         m_measureAnchorPixelY = mouseY;
         m_isMeasuringActive   = true;
         //--- Disable chart scroll while measuring
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      else
        {
         //--- Release the measure anchor and clean up all measure objects
         m_isMeasuringActive = false;
         DeleteAllMeasureObjects();
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
      //--- Reset click timer to prevent triple-click retriggering
      m_lastClickTimeMicros = 0;
     }
   else
     {
      //--- Record this click as the first of a potential double-click pair
      m_lastClickTimeMicros = nowMicros;
     }
  }

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

Далее методы построения перекрестия следуют согласованному шаблону отображения, скрытия и обновления. Методы "ShowCrossVertical" и "ShowCrossHorizontal" делают видимыми свои однопиксельные холсты, а методы обновления перемещают их в координаты экрана курсора. Тот же паттерн применяется к меткам оси цены и времени.

Метод "DrawAndPositionAxisLabel" — это общая утилита для отображения меток цены и времени. Мы считываем цвета переднего и заднего плана графика, измеряем размеры текста метки, изменяем размер холста, чтобы он соответствовал размеру, отрисовываем текст в сырой пиксельный буфер с помощью TextOut в формате COLOR_FORMAT_XRGB_NOALPHA для чистой отрисовки, копируем эти пиксели на холст с полным альфа-каналом, рисуем прямоугольник-границу сверху и располагаем метку у правого края оси для цены или у нижнего края оси для времени. Метод "UpdateCrosshairAxisLabels" вызывает это для обеих меток, используя текущую цену и время бара, отформатированные с помощью функций DoubleToString и TimeToString

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

Метод "UpdateMagnifierPosition" размещает лупу правее и выше от курсора с резервным позиционированием на случай, если она выйдет за пределы экрана, пропуская перерисовку линзы, если курсор не переместился. Метод "DrawMagnifierLensContent" является наиболее сложной операцией отрисовки. Мы очищаем холст лупы, заполняем круговой фон, обрезанный по радиусу линзы, получаем значения OHLC вокруг бара под курсором с помощью CopyRates и iBarShift, а затем рисуем каждую свечу внутри линзы, преобразуя координаты баров в пиксельные позиции увеличенной линзы. Тени свечи, заливки тела и границы тела рисуются с круговым отсечением так, чтобы они оставались в пределах границ линзы. Затем, если это включено, мы рисуем линии bid и ask с помощью SymbolInfoDouble, отображаем сглаженную кольцевую рамку с расчетом альфа-канала, рисуем едва заметное пунктирное перекрестие в центре линзы и завершаем отображением ценовой метки в нижней части линзы.

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

Вспомогательные методы "HideAllCrosshairElements" и "ShowAllCrosshairElements" последовательно вызывают все шесть отдельных методов отображения или скрытия. Метод "DeleteAllMeasureObjects" скрывает холсты с измерениями и удаляет плавающую информационную метку. Наконец, метод "HandleCrosshairDoubleClick" обнаруживает двойные щелчки с помощью функции GetMicrosecondCount с порогом в 500 миллисекунд. При первом двойном щелчке он блокирует координаты привязки измерения и отключает прокрутку графика, а при втором освобождает привязку, очищает все объекты измерения и восстанавливает прокрутку. Наконец, мы интегрируем перекрестие в оболочку верхнего уровня для обновления в реальном времени.

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

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

//+------------------------------------------------------------------+
//| Clean up crosshair and measure mode when switching tools         |
//+------------------------------------------------------------------+
void CToolsSidebar::CleanupCrosshairOnToolSwitch()
  {
   //--- Only clean up if the crosshair was the active tool or measure mode is locked
   if (m_currentActiveTool == TOOL_CROSSHAIR || m_isMeasuringActive)
     {
      HideAllCrosshairElements();
      if (m_isMeasuringActive)
        {
         //--- Release the measure anchor and restore chart scrolling
         m_isMeasuringActive = false;
         DeleteAllMeasureObjects();
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }
  }

//+------------------------------------------------------------------+
//| Update all crosshair and measure canvases for the cursor position|
//+------------------------------------------------------------------+
void CToolsSidebar::HandleCrosshairMouseMove(int mouseX, int mouseY, bool overSidebar, bool overFlyout)
  {
   //--- Skip if the crosshair tool is not active
   if (m_currentActiveTool != TOOL_CROSSHAIR) return;
   //--- Hide all crosshair elements when the cursor is over a panel
   if (overSidebar || overFlyout) { HideAllCrosshairElements(); return; }
   //--- Show all crosshair elements when the cursor is on the chart
   ShowAllCrosshairElements();
   datetime barTime; double barPrice; int subWindow;
   if (ChartXYToTimePrice(m_chartId, mouseX, mouseY, subWindow, barTime, barPrice))
     {
      //--- Update all crosshair line and label positions
      UpdateCrossVerticalPosition(mouseX);
      UpdateCrossHorizontalPosition(mouseY);
      UpdateCrosshairAxisLabels(mouseX, mouseY, barTime, barPrice);
      UpdateReticlePosition(mouseX, mouseY);
      UpdateMagnifierPosition(mouseX, mouseY, barTime, barPrice);
      //--- Update measure mode elements if measuring is active
      if (m_isMeasuringActive)
        {
         int fx = 0, fy = 0;
         if (ChartTimePriceToXY(m_chartId, 0, m_measureAnchorTime, m_measureAnchorPrice, fx, fy))
           {
            ShowMeasureLines();
            UpdateMeasureVerticalPosition(fx);
            UpdateMeasureHorizontalPosition(fy);
            UpdateMeasureAnchorLabels();
           }
         UpdateMeasureDiagonalLine(mouseX, mouseY);
         UpdateMeasurementInfoLabel(mouseX, mouseY, barTime, barPrice);
        }
      ChartRedraw();
     }
  }

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

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


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

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

TOOLS PALETTE RETICLE AND LENS BACKTEST GIF

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


Заключение

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

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

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

Прикрепленные файлы |
Последние комментарии | Перейти к обсуждению на форуме трейдеров (2)
tdx_dahua@163.com
tdx_dahua@163.com | 13 июн. 2026 в 13:51
Диаграмма больше не реагирует на событие перетаскивания.
Allan Munene Mutiiria
Allan Munene Mutiiria | 13 июн. 2026 в 19:01
tdx_dahua@163.com #:
Диаграмма больше не реагирует на событие перетаскивания.
Здравствуйте. Спасибо за отзыв. Да, это предусмотренное поведение, призванное обеспечить плавную работу при взаимодействии с инструментами. Кнопки со стрелками продолжают работать, и вы можете использовать их для прокрутки графика. Мы добавим эту функцию, когда перейдем от EA к полноценному инструменту Utility и завершим окончательную настройку и оптимизацию. На данный момент это предусмотренное поведение. Вы можете отключить эту функцию на уровне слоя событий графика, но это может привести к конфликту между событиями основного графика и нашими объектами холста, если слой не будет правильно переключен. Спасибо.
MLP (многослойный перцептрон) внутри советника MQL5: Обучение на истории без Python и без файлов весов MLP (многослойный перцептрон) внутри советника MQL5: Обучение на истории без Python и без файлов весов
Статья показывает, как перенести MLP-фильтр советника GridSurvivor из офлайн-обучения в Python в полностью встроенное обучение в MQL5. Сеть тренируется на истории текущего символа и таймфрейма, периодически переобучается и используется как последний фильтр сигналов. Подход исключает внешние файлы и рассинхрон нормализации, делая советник самодостаточным и воспроизводимым.
Управление позициями: Безопасный пирамидинг с единым стопом в MQL5 Управление позициями: Безопасный пирамидинг с единым стопом в MQL5
В этой статье представлен CPyramidEngine – переиспользуемый класс на языке MQL5, который добавляет в любой советник дисциплинированное пирамидирование и требует для интеграции всего около шести изменений в коде. Движок обеспечивает соблюдение трех ограничений: размеры лотов должны строго уменьшаться, единый стоп – сдвигаться после каждого добавления, а каждая модификация – проходить проверку на уровне брокера. В статье разбираются типичные сценарии отказа наивных реализаций и показывается, как по мере добавления позиций сохранять общий риск по счету измеримым и контролируемым.
Разработка инструментария для анализа Price Action (Часть 70): Автоматическое исполнение сделок по сигналам паттерна "флаг" Разработка инструментария для анализа Price Action (Часть 70): Автоматическое исполнение сделок по сигналам паттерна "флаг"
В статье описаны буферная архитектура сигналов для пробоев паттерна "флаг" и советник, который ее использует. Стрелки пробоя и высота флагштока записываются в отдельные буферы только после подтверждения, что предотвращает перерисовку и неоднозначность. Советник считывает данные из буферов через CopyBuffer(), проверяет сигналы с помощью настраиваемых фильтров и исполняет сделки с фиксированными или динамическими уровнями SL/TP.
Разработка инструментария для анализа Price Action (Часть 69): Обнаружение паттерна "флаг" в MQL5 Разработка инструментария для анализа Price Action (Часть 69): Обнаружение паттерна "флаг" в MQL5
В этой статье показано, как преобразовать субъективное распознавание паттерна "флаг" в воспроизводимую логику на языке MQL5 для графиков в реальном времени. Она объединяет нормализованную по ATR силу флагштока, ограничения на откат, проверку структуры консолидации, подтверждение пробоя и контроль перекрытия. В результате читатель получает практический подход, который строит адаптивные каналы и зоны, эффективно обновляет активные сетапы и при необходимости выдает алерты о новых подтвержденных паттернах.