Торговые инструменты на MQL5 (Часть 32): Перекрестие, лупа и режим измерения
Введение
Выбрать инструмент и разместить его на графике можно без проблем. Однако после того, как объект уже нанесён, вы остаётесь один на один с чистым движением цены, без точного способа проверить, что находится под курсором. Нет перекрестия для отслеживания вашего точного положения. Вы не видите меток осей, показывающих цену и время при наведении курсора. Нет увеличенного вида для чтения плотно расположенных свечей без увеличения всего графика. Также нет быстрого способа измерить расстояние между двумя точками в барах и пипсах. Без этих средств навигации палитра хорошо обрабатывает вывод, но не даёт инструментов анализа на стороне взаимодействия с пользователем. Эта статья написана для разработчиков MetaQuotes Language 5 (MQL5), создающих интерактивные боковые панели, и алгоритмических трейдеров, использующих такие инструменты.
В своей предыдущей статье (Часть 31) мы добавили полную интерактивность в палитру инструментов. Мы реализовали выдвижную панель для выбора инструментов и обработчик событий графика, маршрутизирующий все взаимодействия с мышью и клавиатурой. Мы также добавили графический движок, поддерживающий размещение объектов одним, двумя и тремя щелчками мыши. Также были включены перетаскивание панелей с привязкой к краям, изменение размера нижнего края, прокручиваемые списки, подсветка при наведении курсора и переключение тем в реальном времени. В части 32 мы представляем класс менеджера перекрестия. Он добавляет одиннадцать слоев холста для наложения ретикула с делениями, линий перекрестий во всю ширину и высоту графика, а также меток оси цены и времени. Он также содержит круговую лупу, отображающую увеличенное содержимое свечей, и режим измерения двойным щелчком с якорными маркерами, диагональной линией и плавающей статистикой баров и пипсов. Мы рассмотрим следующие темы:
- Что привносят ретикул с делениями и лупа на график
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет система перекрестия с ретикулом с делениями, метками осей, увеличительным стеклом и режимом измерения, который позволяет мгновенно подсчитывать количество баров и определять расстояние в пипсах между любыми двумя точками на графике.
Что привносят ретикул с делениями и лупа на график
Предыдущие части давали боковой панели возможность выбирать и размещать инструменты рисования, но как только вы перемещаете курсор на график, визуальная обратная связь о том, куда вы указываете, отсутствует. Стандартное перекрестие MetaTrader 5 существует, но его нельзя настроить, и оно не поддерживает увеличение. Мы восполним этот пробел в этой статье.
Добавленная нами система перекрестий состоит из нескольких многослойных элементов, которые работают вместе. Ретикульное наложение рисует крестики с делениями, смещенные относительно центра курсора, обеспечивая точный ориентир для наведения и не перекрывая саму целевую точку. Горизонтальные линии во всю ширину и вертикальные линии во всю высоту простираются по всему графику от положения курсора, а метки осей цены и времени привязываются к правому и нижнему краям, показывая точное значение под курсором.
Круглая лупа следует за курсором и отображает увеличенное изображение окружающих свечей внутри круглой области с рамкой, включая тени свечи, тела, линии bid и ask, а также ценовую метку, что позволяет разбирать плотные скопления свечей, не меняя уровень масштабирования графика. Двойной щелчок устанавливает точку привязки, после чего диагональная линия соединяет точку привязки с движущимся курсором, а плавающая метка отображает количество баров, расстояние в пипсах и разницу цен в реальном времени.
На графике это означает, что вы можете навести курсор на любую область и мгновенно увидеть точную цену и время, изучить плотные скопления свечей через лупу, не теряя контекста графика, и быстро измерить расстояние между двумя точками двойным щелчком. При наведении курсора на боковую панель или выдвижную панель все элементы перекрестия автоматически скрываются, чтобы не мешать выбору инструментов, а переключение темы перерисовывает каждый холст перекрестия в новом цвете переднего плана.
Мы достигнем этого за четыре шага. Во-первых, мы добавим восемь входных параметров для настройки ретикула, лупы и меток осей. Во-вторых, мы добавляем метод рисования линий по алгоритму Брезенхема для построения измерительной диагонали. В-третьих, мы добавляем одиннадцать новых слоев холста, управляемых специальным классом менеджера перекрестий, вставленным между отрисовщиком боковой панели и обработчиком событий графика. Наконец, мы обновляем обработчик событий и оболочку верхнего уровня для отслеживания перекрестия мыши, обнаружения двойного щелчка и обновления холста с учетом темы. Наглядное представление результата приведено ниже.

Реализация средствами 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-изображения.

Во время тестирования метки перекрестия и оси плавно обновлялись в реальном времени. Лупа корректно отображала свечи с тенями, телами и линиями bid/ask. Режим измерения фиксировался и разблокировался без проблем при двойном щелчке, при этом диагональная линия, якорные маркеры и плавающая статистика баров и пипсов следовали за курсором между фиксациями.
Заключение
В заключение, мы улучшили программу "Палитра инструментов", добавив полноценную систему ретикула перекрестия и лупы на MQL5. Мы представили новый класс менеджера перекрестия с одиннадцатью дополнительными слоями холста, охватывающими наложение ретикула с делениями, полноэкранные линии перекрестия, метки осей цены и времени, круглую лупу, отображающую увеличенное содержимое свечи с линиями bid/ask и сглаженной рамкой, а также режим измерения двойным щелчком с якорными маркерами, диагональной линией Брезенхема и плавающей статистикой, показывающей количество баров, расстояние в пипсах и разницу цен. Все элементы перекрестия автоматически скрываются при наведении курсора на боковую панель или выдвижную панель и обновляют свои цвета при переключении темы. После прочтения этой статьи вы сможете:
- Точно анализировать любой участок графика, используя ретикул перекрестия и метки осей
- Разбирать плотные скопления свечей через лупу, не увеличивая масштаб всего графика
- Мгновенно измерять расстояние между любыми двумя точками графика, используя двойной щелчок по привязке, с подсчетом баров и статистикой пипсов.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22233
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
MLP (многослойный перцептрон) внутри советника MQL5: Обучение на истории без Python и без файлов весов
Управление позициями: Безопасный пирамидинг с единым стопом в MQL5
Разработка инструментария для анализа Price Action (Часть 70): Автоматическое исполнение сделок по сигналам паттерна "флаг"
Разработка инструментария для анализа Price Action (Часть 69): Обнаружение паттерна "флаг" в MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Диаграмма больше не реагирует на событие перетаскивания.