Торговые инструменты на MQL5 (Часть 31): Создание интерактивной палитры инструментов в MQL5
Введение
У вас уже есть отлично отображаемая "Палитра инструментов" на графике MT5 — чистая компоновка, сглаженные значки и две темы — но это всего лишь визуальная оболочка: щелчки ничего не делают. Недостающим элементом является слой взаимодействия, который превращает отрисовку в поведение: надежная проверка попадания по bitmap label-объектам, обработчик событий графика, который обрабатывает события CHARTEVENT_*, а также детерминированный конечный автомат, который сопоставляет ввод пользователя с выбором инструмента, манипуляцией панелью и размещением объектов.
Эта статья написана для разработчиков MetaQuotes Language 5(MQL5) и алгоритмических трейдеров, которым нужен проверяемый, готовый к промышленному использованию интерактивный слой поверх пользовательского интерфейса. Наша цель конкретна: реализовать единую точку входа "OnEvent"/"OnChartEvent" и вспомогательные классы, обеспечивающие следующие минимальные сценарии использования:
- Выбор инструмента и размещение объекта графика
- Прокрутка переполненных категорий
- Перетаскивание панели и привязка её к краям
- Изменение размера панели снизу
- Смена темы в реальном времени
Для этого мы расширяем архитектуру из Части 30 до десяти взаимодействующих классов: реестр инструментов, слои холста (включая холсты выдвижной панели в высоком разрешении), компоновка и проверка попаданий, менеджер всплывающих окон, рендерер, маршрутизатор событий и механизм рисования, поддерживающий размещение в один, два и три клика. На протяжении всей статьи мы рассматриваем контракт взаимодействия (activeTool, clicksRequired, последовательность размещения) как единственный источник истины, поэтому поведение является тестируемым и расширяемым. Мы рассмотрим следующие темы:
- От статической боковой панели к интерактивной системе рисования
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у вас будет полностью интерактивная боковая панель MQL5 с выдвижной панелью выбора инструментов, прокручиваемыми списками, позиционированием с помощью перетаскивания и привязки, а также механизмом рисования с многокликовым вводом, который размещает объекты графика непосредственно из палитры.
От статической боковой панели к интерактивной системе рисования
В предыдущей части мы создали боковую панель, которая отображает значки категорий со сглаженными углами и поддержкой двух тем, но каждая кнопка неактивна. Нажатие на категорию ничего не делает, панель остается неподвижной, и нет пути от выбора инструмента к фактическому рисованию на графике. Разрыв между просто отрисованным интерфейсом и действительно пригодным к использованию интерфейсом как раз и закрывает интерактивный слой. Именно его мы преодолеваем в этой статье.
Обновление предоставляет три основные возможности. Во-первых, выпадающие меню, которые появляются при наведении курсора на кнопку категории, отображая отдельные инструменты внутри этой группы со значками, метками, подсветкой при наведении и поддержкой прокрутки для более длинных списков. Во-вторых, полноценная система взаимодействия с мышью, позволяющая перетаскивать панель за область захвата, изменять ее размер от нижнего края, прокручивать переполненные категории с помощью колеса мыши или ползунка прокрутки, переключать тему по щелчку и закрывать панель. В-третьих, механизм рисования на графике, который преобразует выбор инструментов в реальные объекты графика, обрабатывая размещение одним щелчком, например, горизонтальные линии и стрелки, размещение двумя щелчками, например, линии тренда и прямоугольники, и размещение тремя щелчками, например, каналы и вилы.
На графике это означает, что вы можете навести курсор на категорию "линии", выбрать линию тренда из выдвижной панели, а затем щелкнуть две точки на графике, чтобы разместить ее. Если боковая панель блокирует движение цены, возьмитесь за точки захвата и перетащите ее к противоположному краю, где она прилипнет вплотную к краю. Когда категория активна, ее кнопка подсвечивается синим цветом с акцентной полосой, чтобы вы всегда знали, к какой группе принадлежит ваш текущий инструмент. Изменение размера панели снизу позволяет отображать меньше категорий на небольших экранах, не теряя при этом доступа к остальным категориям из-за прокрутки.
Мы расширим определения и перечисления значков, чтобы охватить все тридцать пять инструментов. Мы введем структуру определения инструментов и перестроим реестр для использования динамических массивов инструментов. Мы также расширим набор цветовых тем, добавим класс выдвижной панели, создадим обработчик событий графика и реализуем механизм рисования с несколькими щелчками мышью. Вкратце, вот иллюстрация результата, на который мы рассчитываем.

Реализация средствами MQL5
Расширение определений значков, перечислений, входных данных и структур
Для поддержки полноценной интерактивной системы рисования мы сначала расширяем базовые определения, добавляя отдельные значки инструментов, исчерпывающее перечисление типов инструментов, новые входные параметры и реструктурированные типы данных.
//+------------------------------------------------------------------+ //| Tools Palette Part 3.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 //--- Define icon for each individual drawing tool using font/char pairs SIconDefinition ICON_TOOL_POINTER = { "Wingdings 3", (uchar)'-' }; // Pointer tool icon SIconDefinition ICON_TOOL_CROSSHAIR = { "Wingdings", (uchar)'W' }; // Crosshair tool icon SIconDefinition ICON_TOOL_TRENDLINE = { "Wingdings 3", (uchar)'&' }; // Trend line tool icon SIconDefinition ICON_TOOL_HLINE = { "Wingdings 3", (uchar)'"' }; // Horizontal line tool icon SIconDefinition ICON_TOOL_VLINE = { "Wingdings 3", (uchar)'#' }; // Vertical line tool icon SIconDefinition ICON_TOOL_RAY = { "Wingdings 3", (uchar)'&' }; // Ray line tool icon SIconDefinition ICON_TOOL_EXTENDED_LINE = { "Wingdings 3", (uchar)'1' }; // Extended line tool icon SIconDefinition ICON_TOOL_INFO_LINE = { "Wingdings 3", (uchar)'2' }; // Info/measure line tool icon SIconDefinition ICON_TOOL_PARALLEL_CH = { "Wingdings 3", (uchar)'H' }; // Parallel channel tool icon SIconDefinition ICON_TOOL_REGRESSION_CH = { "Wingdings 3", (uchar)'I' }; // Regression channel tool icon SIconDefinition ICON_TOOL_STDDEV_CH = { "Wingdings 3", (uchar)'J' }; // Standard deviation channel tool icon SIconDefinition ICON_TOOL_PITCHFORK = { "Wingdings 3", (uchar)'H' }; // Andrew's pitchfork tool icon SIconDefinition ICON_TOOL_SCHIFF = { "Wingdings 3", (uchar)'I' }; // Schiff pitchfork tool icon SIconDefinition ICON_TOOL_MOD_SCHIFF = { "Wingdings 3", (uchar)'K' }; // Modified Schiff pitchfork tool icon SIconDefinition ICON_TOOL_GANN_LINE = { "Wingdings 3", (uchar)'&' }; // Gann line tool icon SIconDefinition ICON_TOOL_GANN_FAN = { "Wingdings 3", (uchar)'0' }; // Gann fan tool icon SIconDefinition ICON_TOOL_GANN_GRID = { "Wingdings", (uchar)'i' }; // Gann grid tool icon SIconDefinition ICON_TOOL_FIBO_RET = { "Wingdings", (uchar)'[' }; // Fibonacci retracement tool icon SIconDefinition ICON_TOOL_FIBO_EXP = { "Wingdings 3", (uchar)'&' }; // Fibonacci expansion tool icon SIconDefinition ICON_TOOL_FIBO_CH = { "Wingdings 3", (uchar)'H' }; // Fibonacci channel tool icon SIconDefinition ICON_TOOL_FIBO_TZ = { "Wingdings 3", (uchar)'#' }; // Fibonacci time zones tool icon SIconDefinition ICON_TOOL_FIBO_FAN = { "Wingdings 3", (uchar)'J' }; // Fibonacci fan tool icon SIconDefinition ICON_TOOL_FIBO_ARCS = { "Wingdings", (uchar)'l' }; // Fibonacci arcs tool icon SIconDefinition ICON_TOOL_RECTANGLE = { "Wingdings", (uchar)'o' }; // Rectangle tool icon SIconDefinition ICON_TOOL_TRIANGLE = { "Wingdings 3", (uchar)'p' }; // Triangle tool icon SIconDefinition ICON_TOOL_ELLIPSE = { "Wingdings", (uchar)'l' }; // Ellipse tool icon SIconDefinition ICON_TOOL_TEXT = { "Webdings", (uchar)'>' }; // Text label tool icon SIconDefinition ICON_TOOL_ARROW_UP = { "Wingdings", (uchar)225 }; // Arrow up tool icon SIconDefinition ICON_TOOL_ARROW_DOWN = { "Wingdings", (uchar)226 }; // Arrow down tool icon SIconDefinition ICON_TOOL_THUMB_UP = { "Wingdings", (uchar)'C' }; // Thumbs up tool icon SIconDefinition ICON_TOOL_THUMB_DOWN = { "Wingdings", (uchar)'D' }; // Thumbs down tool icon SIconDefinition ICON_TOOL_PRICE_LABEL = { "Wingdings", (uchar)234 }; // Left price label tool icon SIconDefinition ICON_TOOL_STOP_SIGN = { "Wingdings", (uchar)251 }; // Stop sign tool icon SIconDefinition ICON_TOOL_CHECK_MARK = { "Wingdings", (uchar)252 }; // Check mark tool icon enum TOOL_TYPE { TOOL_NONE = 0, // No tool active TOOL_POINTER, // Default pointer cursor TOOL_CROSSHAIR, // Crosshair / measure cursor TOOL_TRENDLINE, // Trend line drawing tool TOOL_HLINE, // Horizontal line drawing tool TOOL_VLINE, // Vertical line drawing tool TOOL_RAY, // Ray line drawing tool TOOL_EXTENDED_LINE, // Extended (infinite) line drawing tool TOOL_INFO_LINE, // Info / measure line drawing tool TOOL_PARALLEL_CHANNEL, // Parallel channel drawing tool TOOL_REGRESSION_CHANNEL,// Regression channel drawing tool TOOL_STDDEV_CHANNEL, // Standard deviation channel drawing tool TOOL_PITCHFORK, // Andrew's pitchfork drawing tool TOOL_SCHIFF_PITCHFORK, // Schiff pitchfork drawing tool TOOL_MOD_SCHIFF, // Modified Schiff pitchfork drawing tool TOOL_GANN_LINE, // Gann line drawing tool TOOL_GANN_FAN, // Gann fan drawing tool TOOL_GANN_GRID, // Gann grid drawing tool TOOL_FIBO_RETRACEMENT, // Fibonacci retracement drawing tool TOOL_FIBO_EXPANSION, // Fibonacci expansion drawing tool TOOL_FIBO_CHANNEL, // Fibonacci channel drawing tool TOOL_FIBO_TIMEZONES, // Fibonacci time zones drawing tool TOOL_FIBO_FAN, // Fibonacci fan drawing tool TOOL_FIBO_ARCS, // Fibonacci arcs drawing tool TOOL_RECTANGLE, // Rectangle shape drawing tool TOOL_TRIANGLE, // Triangle shape drawing tool TOOL_ELLIPSE, // Ellipse shape drawing tool TOOL_TEXT, // Text label annotation tool TOOL_ARROW_UP, // Arrow up annotation tool TOOL_ARROW_DOWN, // Arrow down annotation tool TOOL_THUMB_UP, // Thumbs up annotation tool TOOL_THUMB_DOWN, // Thumbs down annotation tool TOOL_PRICE_LABEL, // Left price label annotation tool TOOL_STOP_SIGN, // Stop sign annotation tool TOOL_CHECK_MARK // Check mark annotation tool }; input int FlyoutIconSize = 22; // Flyout Icon Size (pt) input int FlyoutLabelSize = 15; // Flyout Label Font Size (pt) input int FlyoutTitleSize = 14; // Flyout Title Font Size (pt) input int MouseScrollSpeed = 8; // Mouse Scroll Step (px) //+------------------------------------------------------------------+ //| Tool definition structure | //+------------------------------------------------------------------+ struct ToolDefinition { TOOL_TYPE toolType; // Unique tool type identifier string toolLabel; // Display label shown in the flyout panel string iconFontName;// Font name used to render the tool icon uchar iconCharCode;// Character code of the tool icon glyph string tooltipText; // Tooltip string shown on hover }; //+------------------------------------------------------------------+ //| Category definition structure | //+------------------------------------------------------------------+ struct CategoryDefinition { string categoryLabel; // Display label for the category string iconFontName; // Font name used to render the category icon uchar iconCharCode; // Character code of the category icon glyph ToolDefinition tools[]; // Dynamic array of tools belonging to this category }; //+------------------------------------------------------------------+ //| Theme color set structure | //+------------------------------------------------------------------+ struct ThemeColorSet { color sidebarBackground; // Background fill color of the sidebar panel color sidebarBorder; // Outline border color of the sidebar panel color buttonHoverBackground; // Background fill when a category button is hovered color buttonActiveBackground; // Background fill when a category button is active color buttonIconColor; // Default color used to render category icons color buttonIconActiveColor; // Icon color when the button is in active state color flyoutBackground; // Background fill color of the flyout panel color flyoutBorder; // Outline border color of the flyout panel color flyoutItemHoverBackground; // Background fill of a hovered flyout item row color flyoutTextColor; // Default text color of flyout item labels color flyoutTextActiveColor; // Text color of the active flyout item label color flyoutTitleColor; // Color of the flyout panel title text color gripDotsColor; // Color of the drag-grip dot indicators color closeButtonHoverColor; // Background fill of the close button on hover color themeButtonHoverColor; // Background fill of the theme button on hover color separatorColor; // Color of the horizontal separator lines color accentBarColor; // Color of the active tool accent bar indicator color scrollArrowColor; // Default color of the scroll thumb pill color scrollArrowHoverColor; // Color of the scroll thumb pill on hover };
Начнем с объявления тридцати пяти отдельных определений значков инструментов, используя ту же структуру "SIconDefinition", что и в предыдущей части, каждое из которых сопоставляет конкретный инструмент рисования с его шрифтом и кодом символа. Эти определения охватывают курсоры, линии, каналы, вилы, инструменты Ганна, инструменты Фибоначчи, фигуры и аннотации, и они будут отображаться в выдвижной панели при разворачивании категории.
Далее мы вводим перечисление "TOOL_TYPE", которое присваивает уникальный идентификатор каждому инструменту рисования в системе. Начиная с "TOOL_NONE" для отсутствия активного инструмента и "TOOL_POINTER" для курсора по умолчанию, оно перечисляет все тридцать пять инструментов вплоть до "TOOL_CHECK_MARK". Это перечисление используется всем конвейером взаимодействия и рисования для отслеживания того, какой инструмент выбрал пользователь.
Мы также добавили новые входные параметры. Размер значка выдвижной панели, размер шрифта метки и размер шрифта заголовка позволяют пользователю управлять внешним видом текста во всплывающей панели, а скорость прокрутки мыши регулирует скорость прокрутки боковой панели и списков во всплывающем окне за одно нажатие колесика мыши.
Далее мы представляем структуру "ToolDefinition", которая объединяет тип инструмента, отображаемую метку, шрифт значка, код символа и всплывающую подсказку в единый блок. Это делает каждый инструмент самодостаточным элементом данных. Структура "CategoryDefinition" также реструктурирована: предыдущий логический флаг для нескольких инструментов заменен динамическим массивом записей "ToolDefinition". Каждая категория теперь напрямую содержит полный список инструментов, поэтому проверка наличия нескольких инструментов в категории сводится к простому считыванию размера массива.
Наконец, мы расширяем структуру "ThemeColorSet" с пяти полей до девятнадцати. Новые поля охватывают следующие параметры: фон кнопок при наведении курсора и активный фон, цвета активных значков, фон и границу выдвижной панели, цвета элементов выдвижной панели при наведении курсора и текста, цвет заголовка, цвета кнопок закрытия и темы при наведении курсора, цвет акцентной полосы и цвета ползунка прокрутки как в состоянии по умолчанию, так и в состоянии при наведении курсора. Это обеспечивает каждому интерактивному элементу в боковой панели и выдвижной панели полную визуальную обратную связь с учетом темы. Далее мы добавим функцию в класс примитивов, которая поможет в отрисовке указателя выдвижной панели.
Добавление растеризации треугольников к примитивам холста
Выдвижная панель использует треугольник-указатель для визуальной связи с категорией боковой панели, которая ее открыла, поэтому нам нужен метод для заливки треугольных фигур с высоким разрешением.
//+------------------------------------------------------------------+ //| Fill a triangle using scanline rasterization at high resolution | //+------------------------------------------------------------------+ void CCanvasPrimitives::FillTriangleHR(CCanvas &canvas, int x0, int y0, int x1, int y1, int x2, int y2, uint argb) { //--- Store triangle vertices as floating-point arrays for scanline processing double vx[3] = { (double)x0, (double)x1, (double)x2 }; double vy[3] = { (double)y0, (double)y1, (double)y2 }; //--- Find vertical bounding extent of the triangle double minY = vy[0], maxY = vy[0]; for (int i = 1; i < 3; i++) { if (vy[i] < minY) minY = vy[i]; if (vy[i] > maxY) maxY = vy[i]; } //--- Iterate over each horizontal scanline within the bounding box for (int scanY = (int)MathCeil(minY); scanY <= (int)MathFloor(maxY); scanY++) { //--- Compute scanline center Y and prepare intersection buffer double cy = (double)scanY + 0.5; double xi[6]; int nc = 0; //--- Compute X intersections with each edge of the triangle for (int i = 0; i < 3; i++) { int ni = (i + 1) % 3; //--- Determine edge vertical extents double eMin = (vy[i] < vy[ni]) ? vy[i] : vy[ni], eMax = (vy[i] > vy[ni]) ? vy[i] : vy[ni]; //--- Skip edges that do not cross the current scanline if (cy < eMin || cy > eMax || MathAbs(vy[ni] - vy[i]) < 1e-12) continue; //--- Compute intersection parameter along the edge double t = (cy - vy[i]) / (vy[ni] - vy[i]); if (t < 0.0 || t > 1.0) continue; //--- Record intersection X coordinate xi[nc++] = vx[i] + t * (vx[ni] - vx[i]); } //--- Sort intersections left to right for (int a = 0; a < nc - 1; a++) for (int b = a + 1; b < nc; b++) if (xi[a] > xi[b]) { double tmp = xi[a]; xi[a] = xi[b]; xi[b] = tmp; } //--- Fill pixels between paired intersection spans for (int p = 0; p + 1 < nc; p += 2) for (int fx = (int)MathCeil(xi[p]); fx <= (int)MathFloor(xi[p + 1]); fx++) canvas.PixelSet(fx, scanY, argb); } }
Мы реализуем метод "FillTriangleHR", который растеризует закрашенный треугольник, используя тот же подход к сканированию строк, который мы уже применяем для четырехугольников. Мы сохраняем три вершины в виде массивов чисел с плавающей запятой, определяем вертикальную границу, а затем проводим горизонтальные строки развертки через треугольник. Для каждой строки развертки мы вычисляем точку пересечения с тремя ребрами, сортируем эти точки пересечения слева направо и заполняем пиксельные промежутки между каждой парой. Это позволяет получить четко заполненный треугольник с высоким разрешением, который после уменьшения разрешения придает указателю выпадающей панели плавный, сглаженный вид на графике. Далее мы расширяем методы темы, чтобы позаботиться о вновь добавленных элементах.
Расширение цветовых схем и добавление переключения тем в реальном времени
Поскольку боковая панель теперь поддерживает состояния при наведении курсора, активные выделения, выдвижные панели и индикаторы прокрутки, система тем должна охватывать все эти интерактивные элементы.
//+------------------------------------------------------------------+ //| Apply color values matching the current theme state | //+------------------------------------------------------------------+ void CThemeManager::ApplyTheme() { //--- Apply dark theme color assignments if (m_isDarkTheme) { m_themeColors.sidebarBackground = C'30,34,45'; // Dark navy background m_themeColors.sidebarBorder = C'200,210,225'; // Light blue-gray border m_themeColors.buttonHoverBackground = C'30,100,200'; // Blue hover background m_themeColors.buttonActiveBackground = C'41,98,255'; // Bright blue active background m_themeColors.buttonIconColor = C'220,225,235'; // Near-white icon color m_themeColors.buttonIconActiveColor = clrWhite; // Pure white active icon m_themeColors.flyoutBackground = C'36,41,54'; // Dark flyout background m_themeColors.flyoutBorder = C'200,210,225'; // Light flyout border m_themeColors.flyoutItemHoverBackground = C'30,100,200'; // Blue flyout row hover m_themeColors.flyoutTextColor = C'200,210,225'; // Light flyout text m_themeColors.flyoutTextActiveColor = clrWhite; // White active flyout text m_themeColors.flyoutTitleColor = C'90,105,130'; // Muted blue-gray title m_themeColors.gripDotsColor = C'90,100,120'; // Muted slate grip dots m_themeColors.closeButtonHoverColor = C'235,55,55'; // Red close button hover m_themeColors.themeButtonHoverColor = C'255,200,50'; // Yellow theme button hover m_themeColors.separatorColor = C'44,50,64'; // Dark separator line m_themeColors.accentBarColor = C'41,98,255'; // Bright blue accent bar m_themeColors.scrollArrowColor = C'120,130,150'; // Muted scroll thumb m_themeColors.scrollArrowHoverColor = clrWhite; // White scroll thumb hover } else { //--- Apply light theme color assignments m_themeColors.sidebarBackground = clrWhite; // White background m_themeColors.sidebarBorder = C'30,35,45'; // Dark border m_themeColors.buttonHoverBackground = C'30,100,200'; // Blue hover background m_themeColors.buttonActiveBackground = C'41,98,255'; // Bright blue active background m_themeColors.buttonIconColor = C'40,45,58'; // Dark icon color m_themeColors.buttonIconActiveColor = clrWhite; // White active icon m_themeColors.flyoutBackground = clrWhite; // White flyout background m_themeColors.flyoutBorder = C'30,35,45'; // Dark flyout border m_themeColors.flyoutItemHoverBackground = C'30,100,200'; // Blue flyout row hover m_themeColors.flyoutTextColor = C'40,45,58'; // Dark flyout text m_themeColors.flyoutTextActiveColor = clrWhite; // White active flyout text m_themeColors.flyoutTitleColor = C'130,140,160'; // Muted gray title m_themeColors.gripDotsColor = C'160,170,185'; // Light gray grip dots m_themeColors.closeButtonHoverColor = C'210,35,35'; // Red close button hover m_themeColors.themeButtonHoverColor = C'150,100,0'; // Amber theme button hover m_themeColors.separatorColor = C'210,215,225'; // Light separator line m_themeColors.accentBarColor = C'41,98,255'; // Bright blue accent bar m_themeColors.scrollArrowColor = C'120,130,145'; // Muted scroll thumb m_themeColors.scrollArrowHoverColor = C'40,45,58'; // Dark scroll thumb hover } } //+------------------------------------------------------------------+ //| Toggle between dark and light theme and reapply colors | //+------------------------------------------------------------------+ void CThemeManager::ToggleTheme() { //--- Flip the active theme flag and reapply color assignments m_isDarkTheme = !m_isDarkTheme; ApplyTheme(); }
Мы расширяем метод "ApplyTheme" с пяти цветовых назначений, которые были в предыдущей части, до девятнадцати. Помимо исходных цветов фона боковой панели, границы, значков, точек захвата и разделителей, теперь мы назначаем цвета для фона кнопок при наведении курсора и активных элементов, состояния активных значков, фона и границы выдвижной панели, цвета текста и элементов выдвижной панели при наведении курсора, цвета заголовка выдвижной панели, красного цвета для кнопки закрытия при наведении курсора, желтого цвета для кнопки темы при наведении курсора (для темного режима) и янтарного (для светлого режима), цвета акцентной полосы для индикатора активного инструмента, а также цвета ползунка прокрутки как в состоянии по умолчанию, так и в состоянии при наведении курсора. Полное покрытие распространяется как на темную, так и на светлую темы, поэтому каждый интерактивный элемент будет визуально реагировать независимо от того, какая тема активна.
Мы также вводим метод "ToggleTheme", который переключает флаг темной темы и немедленно вызывает метод "ApplyTheme" для обновления всех значений цвета. Это позволяет переключать темы в реальном времени, когда пользователь нажимает кнопку темы на боковой панели, без необходимости перезапуска. После этого мы перепишем весь класс реестра, чтобы он также обрабатывал инструменты, что пригодится в будущем, когда мы расширим палитру инструментов, чтобы она содержала больше инструментов.
Перестройка реестра категорий в полный реестр инструментов
В предыдущей части реестра категорий хранились только метки и значки для каждой категории. Теперь мы перестраиваем его в полноценный реестр инструментов, который включает в себя все определения инструментов и предоставляет методы поиска для систем взаимодействия и рисования.
//+------------------------------------------------------------------+ //| CLASS 3 — Register all tool and category definitions | //+------------------------------------------------------------------+ class CToolRegistry : public CThemeManager { protected: CategoryDefinition m_categories[CAT_COUNT]; // Array of all category definitions protected: //--- Populate all categories and their associated tool lists void InitAllCategoriesAndTools(); //--- Append a single tool entry to the given category tool array void AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip); //--- Return the category that owns the given active tool type ENUM_CATEGORY GetCategoryForActiveTool(TOOL_TYPE activeTool); //--- Return the number of chart clicks required to place the given tool int GetRequiredClickCount(TOOL_TYPE toolType); //--- Return the display label string for the given tool type string GetToolLabel(TOOL_TYPE toolType); }; //+------------------------------------------------------------------+ //| Append a single tool entry to a category tool array | //+------------------------------------------------------------------+ void CToolRegistry::AddTool(ToolDefinition &arr[], TOOL_TYPE type, string label, string font, uchar code, string tooltip) { //--- Expand the array by one slot to accommodate the new tool int sz = ArraySize(arr); ArrayResize(arr, sz + 1); //--- Populate all fields of the new tool definition arr[sz].toolType = type; arr[sz].toolLabel = label; arr[sz].iconFontName = font; arr[sz].iconCharCode = code; arr[sz].tooltipText = tooltip; } //+------------------------------------------------------------------+ //| Populate all categories and their associated tool lists | //+------------------------------------------------------------------+ void CToolRegistry::InitAllCategoriesAndTools() { //--- Assign Cursors category definition and reset its tool array m_categories[CAT_CURSORS].categoryLabel = "Cursors"; m_categories[CAT_CURSORS].iconFontName = ICON_CATEGORY_CURSORS.fontName; m_categories[CAT_CURSORS].iconCharCode = ICON_CATEGORY_CURSORS.charCode; ArrayResize(m_categories[CAT_CURSORS].tools, 0); //--- Add pointer and crosshair tools to Cursors AddTool(m_categories[CAT_CURSORS].tools, TOOL_POINTER, "Pointer", ICON_TOOL_POINTER.fontName, ICON_TOOL_POINTER.charCode, "Default Pointer"); AddTool(m_categories[CAT_CURSORS].tools, TOOL_CROSSHAIR, "Crosshair", ICON_TOOL_CROSSHAIR.fontName, ICON_TOOL_CROSSHAIR.charCode, "Crosshair / Measure"); //--- Assign Lines category definition and reset its tool array m_categories[CAT_LINES].categoryLabel = "Lines"; m_categories[CAT_LINES].iconFontName = ICON_CATEGORY_LINES.fontName; m_categories[CAT_LINES].iconCharCode = ICON_CATEGORY_LINES.charCode; ArrayResize(m_categories[CAT_LINES].tools, 0); //--- Add all line drawing tools to Lines AddTool(m_categories[CAT_LINES].tools, TOOL_TRENDLINE, "Trend Line", ICON_TOOL_TRENDLINE.fontName, ICON_TOOL_TRENDLINE.charCode, "Trend Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_HLINE, "Horizontal", ICON_TOOL_HLINE.fontName, ICON_TOOL_HLINE.charCode, "Horizontal Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_VLINE, "Vertical", ICON_TOOL_VLINE.fontName, ICON_TOOL_VLINE.charCode, "Vertical Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_RAY, "Ray", ICON_TOOL_RAY.fontName, ICON_TOOL_RAY.charCode, "Ray Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_EXTENDED_LINE, "Extended", ICON_TOOL_EXTENDED_LINE.fontName, ICON_TOOL_EXTENDED_LINE.charCode, "Extended Line"); AddTool(m_categories[CAT_LINES].tools, TOOL_INFO_LINE, "Info Line", ICON_TOOL_INFO_LINE.fontName, ICON_TOOL_INFO_LINE.charCode, "Info / Measure Line"); //--- Assign Channels category definition and reset its tool array m_categories[CAT_CHANNELS].categoryLabel = "Channels"; m_categories[CAT_CHANNELS].iconFontName = ICON_CATEGORY_CHANNELS.fontName; m_categories[CAT_CHANNELS].iconCharCode = ICON_CATEGORY_CHANNELS.charCode; ArrayResize(m_categories[CAT_CHANNELS].tools, 0); //--- Add all channel drawing tools to Channels AddTool(m_categories[CAT_CHANNELS].tools, TOOL_PARALLEL_CHANNEL, "Parallel Channel", ICON_TOOL_PARALLEL_CH.fontName, ICON_TOOL_PARALLEL_CH.charCode, "Parallel Channel"); AddTool(m_categories[CAT_CHANNELS].tools, TOOL_REGRESSION_CHANNEL, "Regression", ICON_TOOL_REGRESSION_CH.fontName, ICON_TOOL_REGRESSION_CH.charCode, "Regression Channel"); AddTool(m_categories[CAT_CHANNELS].tools, TOOL_STDDEV_CHANNEL, "Std Deviation", ICON_TOOL_STDDEV_CH.fontName, ICON_TOOL_STDDEV_CH.charCode, "Standard Deviation Channel"); //--- Assign Pitchfork category definition and reset its tool array m_categories[CAT_PITCHFORK].categoryLabel = "Pitchfork"; m_categories[CAT_PITCHFORK].iconFontName = ICON_CATEGORY_PITCHFORK.fontName; m_categories[CAT_PITCHFORK].iconCharCode = ICON_CATEGORY_PITCHFORK.charCode; ArrayResize(m_categories[CAT_PITCHFORK].tools, 0); //--- Add all pitchfork drawing tools to Pitchfork AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_PITCHFORK, "Andrew's Fork", ICON_TOOL_PITCHFORK.fontName, ICON_TOOL_PITCHFORK.charCode, "Andrew's Pitchfork"); AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_SCHIFF_PITCHFORK, "Schiff Fork", ICON_TOOL_SCHIFF.fontName, ICON_TOOL_SCHIFF.charCode, "Schiff Pitchfork"); AddTool(m_categories[CAT_PITCHFORK].tools, TOOL_MOD_SCHIFF, "Mod. Schiff", ICON_TOOL_MOD_SCHIFF.fontName, ICON_TOOL_MOD_SCHIFF.charCode, "Modified Schiff Pitchfork"); //--- Assign Gann category definition and reset its tool array m_categories[CAT_GANN].categoryLabel = "Gann"; m_categories[CAT_GANN].iconFontName = ICON_CATEGORY_GANN.fontName; m_categories[CAT_GANN].iconCharCode = ICON_CATEGORY_GANN.charCode; ArrayResize(m_categories[CAT_GANN].tools, 0); //--- Add all Gann drawing tools to Gann AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_LINE, "Gann Line", ICON_TOOL_GANN_LINE.fontName, ICON_TOOL_GANN_LINE.charCode, "Gann Line"); AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_FAN, "Gann Fan", ICON_TOOL_GANN_FAN.fontName, ICON_TOOL_GANN_FAN.charCode, "Gann Fan"); AddTool(m_categories[CAT_GANN].tools, TOOL_GANN_GRID, "Gann Grid", ICON_TOOL_GANN_GRID.fontName, ICON_TOOL_GANN_GRID.charCode, "Gann Grid"); //--- Assign Fibonacci category definition and reset its tool array m_categories[CAT_FIBONACCI].categoryLabel = "Fibonacci"; m_categories[CAT_FIBONACCI].iconFontName = ICON_CATEGORY_FIBONACCI.fontName; m_categories[CAT_FIBONACCI].iconCharCode = ICON_CATEGORY_FIBONACCI.charCode; ArrayResize(m_categories[CAT_FIBONACCI].tools, 0); //--- Add all Fibonacci drawing tools to Fibonacci AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_RETRACEMENT, "Retracement", ICON_TOOL_FIBO_RET.fontName, ICON_TOOL_FIBO_RET.charCode, "Fibonacci Retracement"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_EXPANSION, "Expansion", ICON_TOOL_FIBO_EXP.fontName, ICON_TOOL_FIBO_EXP.charCode, "Fibonacci Expansion"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_CHANNEL, "Fib Channel", ICON_TOOL_FIBO_CH.fontName, ICON_TOOL_FIBO_CH.charCode, "Fibonacci Channel"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_TIMEZONES, "Time Zones", ICON_TOOL_FIBO_TZ.fontName, ICON_TOOL_FIBO_TZ.charCode, "Fibonacci Time Zones"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_FAN, "Fib Fan", ICON_TOOL_FIBO_FAN.fontName, ICON_TOOL_FIBO_FAN.charCode, "Fibonacci Fan"); AddTool(m_categories[CAT_FIBONACCI].tools, TOOL_FIBO_ARCS, "Fib Arcs", ICON_TOOL_FIBO_ARCS.fontName, ICON_TOOL_FIBO_ARCS.charCode, "Fibonacci Arcs"); //--- Assign Shapes category definition and reset its tool array m_categories[CAT_SHAPES].categoryLabel = "Shapes"; m_categories[CAT_SHAPES].iconFontName = ICON_CATEGORY_SHAPES.fontName; m_categories[CAT_SHAPES].iconCharCode = ICON_CATEGORY_SHAPES.charCode; ArrayResize(m_categories[CAT_SHAPES].tools, 0); //--- Add all shape drawing tools to Shapes AddTool(m_categories[CAT_SHAPES].tools, TOOL_RECTANGLE, "Rectangle", ICON_TOOL_RECTANGLE.fontName, ICON_TOOL_RECTANGLE.charCode, "Rectangle"); AddTool(m_categories[CAT_SHAPES].tools, TOOL_TRIANGLE, "Triangle", ICON_TOOL_TRIANGLE.fontName, ICON_TOOL_TRIANGLE.charCode, "Triangle"); AddTool(m_categories[CAT_SHAPES].tools, TOOL_ELLIPSE, "Ellipse", ICON_TOOL_ELLIPSE.fontName, ICON_TOOL_ELLIPSE.charCode, "Ellipse"); //--- Assign Annotations category definition and reset its tool array m_categories[CAT_ANNOTATIONS].categoryLabel = "Annotate"; m_categories[CAT_ANNOTATIONS].iconFontName = ICON_CATEGORY_ANNOTATIONS.fontName; m_categories[CAT_ANNOTATIONS].iconCharCode = ICON_CATEGORY_ANNOTATIONS.charCode; ArrayResize(m_categories[CAT_ANNOTATIONS].tools, 0); //--- Add all annotation tools to Annotations AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_TEXT, "Text", ICON_TOOL_TEXT.fontName, ICON_TOOL_TEXT.charCode, "Text Label"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_UP, "Arrow Up", ICON_TOOL_ARROW_UP.fontName, ICON_TOOL_ARROW_UP.charCode, "Arrow Up"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_ARROW_DOWN, "Arrow Down", ICON_TOOL_ARROW_DOWN.fontName, ICON_TOOL_ARROW_DOWN.charCode, "Arrow Down"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_UP, "Thumb Up", ICON_TOOL_THUMB_UP.fontName, ICON_TOOL_THUMB_UP.charCode, "Thumbs Up"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_THUMB_DOWN, "Thumb Down", ICON_TOOL_THUMB_DOWN.fontName, ICON_TOOL_THUMB_DOWN.charCode, "Thumbs Down"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_PRICE_LABEL, "Price Label", ICON_TOOL_PRICE_LABEL.fontName, ICON_TOOL_PRICE_LABEL.charCode, "Left Price Label"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_STOP_SIGN, "Stop Sign", ICON_TOOL_STOP_SIGN.fontName, ICON_TOOL_STOP_SIGN.charCode, "Stop Sign"); AddTool(m_categories[CAT_ANNOTATIONS].tools, TOOL_CHECK_MARK, "Check Mark", ICON_TOOL_CHECK_MARK.fontName, ICON_TOOL_CHECK_MARK.charCode, "Check Mark"); } //+------------------------------------------------------------------+ //| Return the category that owns the given active tool type | //+------------------------------------------------------------------+ ENUM_CATEGORY CToolRegistry::GetCategoryForActiveTool(TOOL_TYPE activeTool) { //--- Return no category for inactive or pointer tool states if (activeTool == TOOL_NONE || activeTool == TOOL_POINTER) return CAT_NONE; //--- Search all categories and their tool lists for a match for (int c = 0; c < CAT_COUNT; c++) for (int t = 0; t < ArraySize(m_categories[c].tools); t++) if (m_categories[c].tools[t].toolType == activeTool) return (ENUM_CATEGORY)c; return CAT_NONE; } //+------------------------------------------------------------------+ //| Return click count required to place the given tool | //+------------------------------------------------------------------+ int CToolRegistry::GetRequiredClickCount(TOOL_TYPE toolType) { switch (toolType) { //--- Cursor tools require no chart clicks case TOOL_POINTER: case TOOL_CROSSHAIR: return 0; //--- Single-click tools are placed with one chart interaction case TOOL_HLINE: case TOOL_VLINE: case TOOL_TEXT: case TOOL_ARROW_UP: case TOOL_ARROW_DOWN: case TOOL_THUMB_UP: case TOOL_THUMB_DOWN: case TOOL_PRICE_LABEL: case TOOL_STOP_SIGN: case TOOL_CHECK_MARK: case TOOL_FIBO_TIMEZONES: return 1; //--- Two-click tools require a start and end point case TOOL_TRENDLINE: case TOOL_RAY: case TOOL_EXTENDED_LINE: case TOOL_INFO_LINE: case TOOL_RECTANGLE: case TOOL_TRIANGLE: case TOOL_ELLIPSE: case TOOL_FIBO_RETRACEMENT: case TOOL_FIBO_EXPANSION: case TOOL_FIBO_FAN: case TOOL_FIBO_ARCS: case TOOL_GANN_LINE: case TOOL_GANN_FAN: case TOOL_GANN_GRID: case TOOL_REGRESSION_CHANNEL: case TOOL_STDDEV_CHANNEL: return 2; //--- Three-click tools require three anchor points case TOOL_PARALLEL_CHANNEL: case TOOL_FIBO_CHANNEL: case TOOL_PITCHFORK: case TOOL_SCHIFF_PITCHFORK: case TOOL_MOD_SCHIFF: return 3; //--- Default to single click for unrecognized tool types default: return 1; } } //+------------------------------------------------------------------+ //| Return the display label string for the given tool type | //+------------------------------------------------------------------+ string CToolRegistry::GetToolLabel(TOOL_TYPE toolType) { //--- Search all categories and tool lists for a label match for (int c = 0; c < CAT_COUNT; c++) for (int t = 0; t < ArraySize(m_categories[c].tools); t++) if (m_categories[c].tools[t].toolType == toolType) return m_categories[c].tools[t].toolLabel; return "None"; }
В этой части мы объявляем класс "CToolRegistry", который заменяет предыдущий "CCategoryRegistry" и наследует от "CThemeManager". Он содержит тот же массив категорий, но теперь объявляет пять защищенных методов: "InitAllCategoriesAndTools" для заполнения всех категорий и их инструментов, "AddTool" в качестве вспомогательного метода для добавления записей инструментов, "GetCategoryForActiveTool" для обратного поиска категории, которой принадлежит данный инструмент, "GetRequiredClickCount" для определения количества кликов по графику, необходимых инструменту, и "GetToolLabel" для получения отображаемого имени инструмента.
Метод "AddTool" расширяет заданный массив инструментов на один слот с помощью функции ArrayResize и заполняет новую запись типом инструмента, меткой, шрифтом значка, кодом символа и всплывающей подсказкой. Это обеспечивает чистоту и согласованность заполнения категорий.
Мы реализуем метод "InitAllCategoriesAndTools", присваивая каждой категории ее метку и значок из глобальных определений значков, обнуляя массив инструментов, а затем многократно вызывая метод "AddTool" для регистрации каждого инструмента. Курсоры получают указатель и перекрестие. Линии получают шесть инструментов от линии тренда до информационной линии. Категории каналов, вил и инструментов Ганна получают по три инструмента. Фибоначчи получает самый большой набор из шести инструментов, охватывающих коррекцию, расширение, канал, временные зоны, веер и дуги. Фигуры содержат прямоугольники, треугольники и эллипсы. Аннотации содержат восемь инструментов от текстовых меток до галочек.
Метод "GetCategoryForActiveTool" возвращает информацию о том, какой категории принадлежит данный активный инструмент, путем поиска по всем категориям и их массивам инструментов, возвращая "CAT_NONE" для неактивных состояний или состояний указателя. Метод "GetRequiredClickCount" использует оператор switch для классификации каждого инструмента по количеству кликов: ноль кликов для курсоров, один клик для горизонтальных линий, вертикальных линий, аннотаций и аналогичных инструментов с одной точкой, два клика для линий тренда, прямоугольников, коррекций Фибоначчи и других инструментов с двумя якорями, а три клика для параллельных каналов, каналов Фибоначчи и вариантов вил. Метод "GetToolLabel" ищет во всех категориях соответствующий тип инструмента и возвращает его отображаемую метку. Теперь мы расширим класс canvases, чтобы расширить слои с выпадающими поверхностями.
Расширение слоя холст с помощью поверхностей выдвижных панелей
Выдвижной панели нужны собственные независимые поверхности для рисования, поэтому мы расширяем класс слоя холст, чтобы управлять четырьмя холстами вместо двух предыдущих.
//+------------------------------------------------------------------+ //| 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 string m_nameSidebar; // Object name of the sidebar bitmap label string m_nameFlyout; // Object name of the flyout bitmap label protected: //--- Create all canvas objects at the given dimensions bool CreateAllCanvases(int w, int h); //--- Destroy all canvas objects and remove chart objects void DestroyAllCanvases(); //--- Resize both sidebar canvases to the given dimensions void ResizeSidebarCanvases(int w, int h); }; //+------------------------------------------------------------------+ //| Create all canvas objects at the given dimensions | //+------------------------------------------------------------------+ bool CCanvasLayer::CreateAllCanvases(int w, int h) { //--- Create the display-resolution sidebar bitmap label canvas if (!m_canvasSidebar.CreateBitmapLabel(0, 0, m_nameSidebar, 0, 0, w, h, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("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; } return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and remove chart objects | //+------------------------------------------------------------------+ void CCanvasLayer::DestroyAllCanvases() { //--- Destroy display canvas and remove its chart object m_canvasSidebar.Destroy(); ObjectDelete(0, m_nameSidebar); //--- Destroy the high-resolution sidebar working canvas m_canvasSidebarHighRes.Destroy(); //--- Destroy flyout canvas and remove its chart object m_canvasFlyout.Destroy(); ObjectDelete(0, m_nameFlyout); //--- Destroy the high-resolution flyout working canvas m_canvasFlyoutHighRes.Destroy(); }
Мы объявляем класс "CCanvasLayer", который теперь наследует от "CToolRegistry" и добавляет два новых члена canvas к существующей паре боковой панели: canvas для выдвижной панели с разрешением экрана и его аналог с высоким разрешением для рендеринга с суперсэмплированием, а также строку, содержащую имя bitmap label-объекта всплывающего окна.
Метод "CreateAllCanvases" теперь создает четыре холста последовательно. После создания дисплея боковой панели и холстов высокого разрешения, как и прежде, мы создаем холст для выдвижной панели в виде bitmap label-объекта с помощью функции CreateBitmapLabel с начальным размером двести на двести пикселей, а затем его рабочий холст высокого разрешения, масштабированный с помощью коэффициента суперсэмплирования. Если какой-либо из четырех способов создания не удается, мы выводим ошибку и возвращаем false.
Метод "DestroyAllCanvases" повторяет это действие, уничтожая холст боковой панели и удаляя его объект графика с помощью ObjectDelete, уничтожая холст боковой панели высокого разрешения, а затем делая то же самое для обоих холстов выдвижной панели. Метод "ResizeSidebarCanvases" остается неизменным по сравнению с предыдущей частью, поскольку изменение размера выдвижной панели обрабатывается динамически во время рендеринга, а не с помощью специального вызова изменения размера. Далее мы расширим макет боковой панели для полной интерактивности; она должна прокручиваться, изменять размер от нижнего края, перетаскиваться и включать ползунок прокрутки.
Расширение компоновки боковой панели с прокруткой, перетаскиванием, изменением размера и проверкой попадания
Класс макета боковой панели значительно расширен, чтобы поддерживать все интерактивные действия, необходимые для этой панели, от прокрутки и перетаскивания до изменения размера и точного определения нажатия мыши.
//+------------------------------------------------------------------+ //| CLASS 5 — Compute and maintain sidebar layout and geometry | //+------------------------------------------------------------------+ class CSidebarLayout : public CCanvasLayer { protected: int m_panelX; // Horizontal position of the sidebar panel int m_panelY; // Vertical position of the sidebar panel int m_sidebarWidth; // Width of the sidebar panel in pixels int m_sidebarHeight; // Height of the sidebar panel in pixels int m_categoryButtonSize; // Size of each category button in pixels int m_categoryButtonPadding; // Vertical gap between category buttons int m_panelCornerRadius; // Corner rounding radius of the panel int m_headerGripHeight; // Height of the top header and grip area ENUM_SNAP_STATE m_snapState; // Current snap alignment state int m_sidebarMaxVisibleCats; // Maximum number of visible category buttons int m_sidebarScrollPixels; // Current vertical scroll offset in pixels int m_sidebarScrollThumbHeight; // Height of the sidebar scroll thumb pill int m_sidebarScrollThinWidth; // Width of the sidebar scroll thumb pill bool m_isSidebarThumbDragging; // Flag indicating scroll thumb drag in progress int m_sidebarThumbDragStartY; // Mouse Y when sidebar thumb drag started int m_sidebarThumbDragStartPixels;// Scroll offset when sidebar thumb drag started bool m_isHoveredSidebarScrollArea;// Flag indicating mouse is over sidebar scroll area bool m_isHoveredSidebarThumb; // Flag indicating mouse is over sidebar scroll thumb bool m_isPanelDragging; // Flag indicating panel drag in progress int m_dragOffsetX; // Mouse X offset from panel origin when drag started int m_dragOffsetY; // Mouse Y offset from panel origin when drag started bool m_isResizingBottomEdge; // Flag indicating bottom resize drag in progress int m_bottomResizeDragStartY; // Mouse Y when bottom resize drag started int m_bottomResizeStartHeight; // Panel height when bottom resize drag started int m_snappedSidebarHeight; // User-set height override while panel is snapped bool m_isBottomResizeHovered; // Flag indicating mouse is over the bottom resize grip protected: //--- Compute and set the sidebar panel height based on available chart space void CalcSidebarHeight(); //--- Compute the Y pixel position of a category button by index int CalcCategoryButtonY(int idx); //--- Compute the top clipping boundary for the category button area int CalcClipTop(); //--- Compute the bottom clipping boundary for the category button area int CalcClipBottom(); //--- Compute total pixel height of all category buttons stacked int CalcSidebarTotalScrollPixels(); //--- Compute the visible viewport pixel height for category buttons int CalcSidebarViewportPixels(); //--- Compute the maximum allowable scroll offset in pixels int CalcSidebarMaxScrollPixels(); //--- Check whether the category button at the given index is within the visible clip area bool IsCategoryButtonVisible(int idx); //--- Attempt to snap the panel to a chart edge based on current position void TrySnapToEdge(); //--- Test whether the given screen coordinates hit the sidebar panel bool HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly); //--- Return the category under the given local coordinates, or CAT_NONE ENUM_CATEGORY HitTestCategoryButton(int lx, int ly); //--- Test whether the given local coordinates hit the grip drag area bool HitTestOverGripArea(int lx, int ly); //--- Test whether the given local coordinates hit the close button area bool HitTestOverCloseButton(int lx, int ly); //--- Test whether the given local coordinates hit the theme toggle button area bool HitTestOverThemeButton(int lx, int ly); //--- Test whether the given local coordinates hit the bottom resize grip bool HitTestOverBottomResizeGrip(int lx, int ly); }; //+------------------------------------------------------------------+ //| Compute and set sidebar height based on available chart space | //+------------------------------------------------------------------+ void CSidebarLayout::CalcSidebarHeight() { //--- Get current chart height in pixels int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Define vertical padding constants int topPad = 8, botPad = 10; //--- Set button gap spacing m_categoryButtonPadding = 6; //--- Handle snapped panel height computation if (m_snapState != SNAP_FLOAT) { //--- Pin panel to fixed Y offset below chart top m_panelY = 30; ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Compute maximum available height below panel top offset int availH = chartH - m_panelY - 8; //--- Compute ideal natural height to fit all category buttons int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Compute minimum height to show at least three category buttons int minH = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Apply user-set snapped height override if present, otherwise use natural size if (m_snappedSidebarHeight > 0) m_sidebarHeight = MathMax(minH, MathMin(MathMin(naturalH, availH), m_snappedSidebarHeight)); else m_sidebarHeight = MathMax(minH, MathMin(naturalH, availH)); } else { //--- Compute natural and maximum height bounds for a floating panel int naturalH = m_headerGripHeight + topPad + CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; int maxH = chartH - m_panelY - 20; int minH = m_headerGripHeight + topPad + 3 * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding + botPad; //--- Clamp the floating panel height within valid bounds if (m_sidebarHeight < minH || m_sidebarHeight > MathMin(naturalH, maxH)) m_sidebarHeight = MathMin(naturalH, maxH); } //--- Compute usable height for the button area int btnAreaH = m_sidebarHeight - m_headerGripHeight - topPad - botPad; //--- Compute total height needed for all buttons at natural spacing int fullBtnH = CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding; //--- All buttons fit: show all and clear scroll offset if (fullBtnH <= btnAreaH) { m_sidebarMaxVisibleCats = CAT_COUNT; m_sidebarScrollPixels = 0; } else { //--- Compute how many buttons fit and clamp scroll offset within valid range m_sidebarMaxVisibleCats = MathMax(3, MathMin(CAT_COUNT, btnAreaH / (m_categoryButtonSize + m_categoryButtonPadding))); m_sidebarScrollPixels = MathMax(0, MathMin(m_sidebarScrollPixels, CalcSidebarMaxScrollPixels())); } } //+------------------------------------------------------------------+ //| Compute Y pixel position of a category button by index | //+------------------------------------------------------------------+ int CSidebarLayout::CalcCategoryButtonY(int idx) { //--- Return scroll-adjusted Y offset below the header grip area return m_headerGripHeight + 8 + idx * (m_categoryButtonSize + m_categoryButtonPadding) - m_sidebarScrollPixels; } //+------------------------------------------------------------------+ //| Compute top clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipTop() { //--- Return Y position just below the header grip bottom edge return m_headerGripHeight + 8; } //+------------------------------------------------------------------+ //| Compute bottom clip boundary for the category button area | //+------------------------------------------------------------------+ int CSidebarLayout::CalcClipBottom() { //--- Return Y position leaving bottom padding inside the panel return m_sidebarHeight - 10; } //+------------------------------------------------------------------+ //| Compute total pixel height of all category buttons stacked | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarTotalScrollPixels() { //--- Return the combined height of all buttons including inter-button gaps return CAT_COUNT * (m_categoryButtonSize + m_categoryButtonPadding) - m_categoryButtonPadding; } //+------------------------------------------------------------------+ //| Compute the visible viewport pixel height for category buttons | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarViewportPixels() { //--- Return the pixel height of the visible button clip region return CalcClipBottom() - CalcClipTop(); } //+------------------------------------------------------------------+ //| Compute the maximum allowable scroll offset in pixels | //+------------------------------------------------------------------+ int CSidebarLayout::CalcSidebarMaxScrollPixels() { //--- Return zero if all buttons fit; otherwise return the overflow amount return MathMax(0, CalcSidebarTotalScrollPixels() - CalcSidebarViewportPixels()); } //+------------------------------------------------------------------+ //| Check whether a category button is within the visible clip area | //+------------------------------------------------------------------+ bool CSidebarLayout::IsCategoryButtonVisible(int idx) { //--- All buttons are visible when scroll is not needed if (m_sidebarMaxVisibleCats >= CAT_COUNT) return true; //--- Compute the button's scroll-adjusted Y position int y = CalcCategoryButtonY(idx); //--- Return true if the button overlaps the clip region return (y + m_categoryButtonSize > CalcClipTop() && y < CalcClipBottom()); } //+------------------------------------------------------------------+ //| Attempt to snap the panel to a chart edge based on position | //+------------------------------------------------------------------+ void CSidebarLayout::TrySnapToEdge() { //--- Get current chart width for right-edge detection int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); ENUM_SNAP_STATE prev = m_snapState; //--- Snap to left edge if panel is within the snap threshold if (m_panelX <= SnapThreshold) { m_snapState = SNAP_LEFT; m_panelX = 0; //--- Clear snapped height override when transitioning from float if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0; } //--- Snap to right edge if panel right boundary is within the snap threshold else if (m_panelX + m_sidebarWidth >= chartW - SnapThreshold) { m_snapState = SNAP_RIGHT; m_panelX = chartW - m_sidebarWidth; if (prev == SNAP_FLOAT) m_snappedSidebarHeight = 0; } else { //--- Set floating state and clear snapped height when leaving a snapped edge m_snapState = SNAP_FLOAT; if (prev != SNAP_FLOAT) { m_snappedSidebarHeight = 0; m_categoryButtonPadding = 6; } } //--- Update the chart object X position to reflect the snapped or free position ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); } //+------------------------------------------------------------------+ //| Test whether screen coordinates hit the sidebar panel | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverSidebar(int mouseX, int mouseY, int &lx, int &ly) { //--- Compute local coordinates relative to the panel origin lx = mouseX - m_panelX; ly = mouseY - m_panelY; //--- Return true if local coordinates fall within the panel bounds return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_sidebarHeight); } //+------------------------------------------------------------------+ //| Return the category under local coordinates, or CAT_NONE | //+------------------------------------------------------------------+ ENUM_CATEGORY CSidebarLayout::HitTestCategoryButton(int lx, int ly) { //--- Reject coordinates outside the category button clip region if (ly < CalcClipTop() || ly >= CalcClipBottom()) return CAT_NONE; //--- Compute horizontal start of the centered button column int btnX = (m_sidebarWidth - m_categoryButtonSize) / 2; //--- Test each visible category button for a hit for (int c = 0; c < CAT_COUNT; c++) { if (!IsCategoryButtonVisible(c)) continue; int btnY = CalcCategoryButtonY(c); //--- Return category if local coordinates fall within the button bounds if (lx >= btnX && lx <= btnX + m_categoryButtonSize && ly >= btnY && ly <= btnY + m_categoryButtonSize && ly < m_sidebarHeight - 8) return (ENUM_CATEGORY)c; } return CAT_NONE; } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the grip drag area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverGripArea(int lx, int ly) { //--- Return true if coordinates fall within the horizontal grip strip return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize && ly < m_categoryButtonSize + 20); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the close button area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverCloseButton(int lx, int ly) { //--- Return true if coordinates fall within the top close button slot return (lx >= 0 && lx < m_sidebarWidth && ly >= 0 && ly < m_categoryButtonSize); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the theme toggle button area | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverThemeButton(int lx, int ly) { //--- Return true if coordinates fall within the theme toggle row return (lx >= 0 && lx < m_sidebarWidth && ly >= m_categoryButtonSize + 20 && ly < m_headerGripHeight); } //+------------------------------------------------------------------+ //| Test whether local coordinates hit the bottom resize grip | //+------------------------------------------------------------------+ bool CSidebarLayout::HitTestOverBottomResizeGrip(int lx, int ly) { //--- Return true if coordinates fall within the bottom resize handle strip return (lx >= 0 && lx < m_sidebarWidth && ly >= m_sidebarHeight - 8 && ly < m_sidebarHeight); }
В этой части мы расширяем класс "CSidebarLayout" за счет существенного набора новых защищенных членов. Помимо исходных параметров позиции, размеров, изменения размера кнопок, радиуса скругления углов, высоты заголовка, состояния привязки и количества видимых категорий из предыдущей части, мы добавляем переменные отслеживания прокрутки, включая текущее смещение прокрутки в пикселях, высоту и ширину ползунка прокрутки, состояние перетаскивания ползунка с начальной позицией и начальным смещением, а также флаги наведения курсора как для области прокрутки, так и для самого ползунка. Мы также добавляем состояние перетаскивания панели с отслеживанием смещения мыши, состояние изменения размера нижнего края с начальной позицией перетаскивания и исходной высотой, переопределение привязки высоты для панелей, измененных пользователем, и флаг изменения размера при наведении курсора на нижний край.
Список методов также расширяется. В дополнение к существующим методам расчета высоты, позиционирования кнопок и определения границ фрагмента, мы добавляем функции "CalcSidebarTotalScrollPixels", которая возвращает суммарную высоту всех кнопок категорий, расположенных друг над другом, "CalcSidebarViewportPixels", которая возвращает высоту видимой области фрагмента, "CalcSidebarMaxScrollPixels", которая вычисляет максимальное смещение прокрутки, а также "IsCategoryButtonVisible", которая проверяет, попадает ли кнопка в видимую область после прокрутки. Мы также добавляем "TrySnapToEdge" для привязки панели к краям графика после перетаскивания, а также шесть методов проверки попадания: "HitTestOverSidebar" для проверки нахождения курсора мыши над панелью, "HitTestCategoryButton" для определения того, какая кнопка категории находится под курсором, и отдельные проверки для области захвата, кнопки закрытия, кнопки темы и нижней области изменения размера.
Метод "CalcSidebarHeight" теперь переключается между состояниями с привязкой и плавающим состоянием, применяя заданную пользователем высоту при привязке и ограничивая плавающие панели допустимыми границами. Когда все кнопки помещаются, он сбрасывает смещение прокрутки. Когда они выходят за пределы допустимого диапазона, он вычисляет, сколько кнопок помещается, и ограничивает прокрутку в пределах диапазона. Метод "CalcCategoryButtonY" теперь вычитает смещение прокрутки, поэтому кнопки сдвигаются вверх по мере прокрутки пользователем. Метод "TrySnapToEdge" проверяет, находится ли панель в пределах порога привязки к любому из краев графика, закрепляет ее в нужном положении, если это так, и сбрасывает значение высоты привязки при переходе из плавающего состояния. Каждый из шести методов проверки попадания мыши проверяет, попадают ли локальные координаты в соответствующую область боковой панели, что позволяет обработчику событий точно определить, с чем взаимодействует пользователь. Далее мы управляем выдвижной панелью выбора инструментов.
Внедрение класса выдвижной панели
Этот совершенно новый класс управляет всплывающей панелью выбора инструментов, которая появляется при наведении курсора на кнопку категории, обрабатывая ее позиционирование, отрисовку, прокрутку и определение нажатия.
//+------------------------------------------------------------------+ //| CLASS 6 — Manage the flyout tool selection panel | //+------------------------------------------------------------------+ class CFlyoutPanel : public CSidebarLayout { protected: int m_flyoutWidth; // Width of the flyout body (excluding pointer triangle) int m_flyoutItemHeight; // Height of each tool item row in the flyout int m_flyoutPadding; // Horizontal and vertical padding inside the flyout int m_flyoutPointerWidth; // Half-height of the pointer triangle int m_flyoutPointerHeight; // Depth (horizontal extent) of the pointer triangle int m_flyoutPointerLocalY; // Local Y center of the pointer tip within the flyout bool m_flyoutPointerOnLeft; // Flag indicating the pointer faces left toward the sidebar bool m_isFlyoutVisible; // Flag indicating the flyout is currently visible ENUM_CATEGORY m_flyoutActiveCat; // Category whose tools are currently shown in the flyout int m_hoveredFlyoutItem; // Index of the hovered flyout item row, or -1 int m_flyoutScrollPixels; // Current vertical scroll offset of the flyout list int m_flyoutMaxVisibleItems; // Maximum number of visible tool rows in the flyout int m_flyoutScrollThumbHeight; // Height of the flyout scroll thumb pill bool m_isFlyoutThumbDragging; // Flag indicating flyout scroll thumb drag in progress int m_flyoutThumbDragStartY; // Mouse Y when flyout thumb drag started int m_flyoutThumbDragStartPixels;// Scroll offset when flyout thumb drag started bool m_isHoveredFlyoutScrollArea; // Flag indicating mouse is over the flyout scroll area bool m_isHoveredFlyoutThumb; // Flag indicating mouse is over the flyout scroll thumb protected: //--- Show the flyout panel for the given category, highlighting the active tool void ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool); //--- Hide the flyout panel and reset its state void HideFlyout(); //--- Draw and composite the full flyout panel for the given category void DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool); //--- Draw the flyout scroll thumb pill overlay onto the display canvas void DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat); //--- Draw the flyout body border at high resolution void DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor); //--- Test whether screen coordinates hit the visible flyout panel bool HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly); //--- Return the flyout item index under the given local coordinates, or -1 int HitTestFlyoutItem(int lx, int ly); }; //+------------------------------------------------------------------+ //| Hide the flyout panel and reset its state | //+------------------------------------------------------------------+ void CFlyoutPanel::HideFlyout() { //--- Reset hover item and scroll offset m_hoveredFlyoutItem = -1; m_flyoutScrollPixels = 0; //--- Clear scroll hover flags m_isHoveredFlyoutScrollArea = false; m_isHoveredFlyoutThumb = false; //--- Hide the flyout chart object from all timeframes ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_NO_PERIODS); //--- Mark the flyout as hidden and clear the active category m_isFlyoutVisible = false; m_flyoutActiveCat = CAT_NONE; } //+------------------------------------------------------------------+ //| Test whether screen coordinates hit the visible flyout panel | //+------------------------------------------------------------------+ bool CFlyoutPanel::HitTestOverFlyout(int mouseX, int mouseY, int &lx, int &ly) { //--- Skip test if flyout is not visible if (!m_isFlyoutVisible) return false; //--- Read the flyout chart object position and size int fx = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE); int fy = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE); int fw = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_XSIZE); int fh = (int)ObjectGetInteger(0, m_nameFlyout, OBJPROP_YSIZE); //--- Compute local coordinates relative to the flyout origin lx = mouseX - fx; ly = mouseY - fy; //--- Return true if the mouse is within the flyout bounds return (mouseX >= fx && mouseX < fx + fw && mouseY >= fy && mouseY < fy + fh); } //+------------------------------------------------------------------+ //| Return flyout item index under local coordinates, or -1 | //+------------------------------------------------------------------+ int CFlyoutPanel::HitTestFlyoutItem(int lx, int ly) { //--- Return no hit if no category is active if (m_flyoutActiveCat == CAT_NONE) return -1; int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); //--- Compute title row height and body left offset for pointer direction int titleH = 26, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); //--- Exclude clicks on the scroll thumb column when scrolling is active if (nTools > m_flyoutMaxVisibleItems) { int tw = m_sidebarScrollThinWidth; if (!m_flyoutPointerOnLeft) { if (lx <= dispBx + tw + 8) return -1; } else { if (lx >= dispBx + m_flyoutWidth - tw - 8) return -1; } } //--- Compute the vertical clip region for item rows int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight; //--- Return no hit if Y is outside the item clip region if (ly < itemClipTop || ly >= itemClipBot) return -1; //--- Compute item index from Y position accounting for scroll offset int idx = (ly - itemClipTop + m_flyoutScrollPixels) / m_flyoutItemHeight; if (idx < 0 || idx >= nTools) return -1; return idx; } //+------------------------------------------------------------------+ //| Show the flyout for the given category with active tool state | //+------------------------------------------------------------------+ void CFlyoutPanel::ShowFlyout(ENUM_CATEGORY cat, TOOL_TYPE activeTool) { //--- Hide flyout and exit if the category has no tools int nTools = ArraySize(m_categories[(int)cat].tools); if (nTools == 0) { HideFlyout(); return; } //--- Reset the flyout scroll offset on each new show m_flyoutScrollPixels = 0; //--- Compute flyout panel height based on visible tool count int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); int flyH = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding; int totalW = m_flyoutWidth + m_flyoutPointerHeight; //--- Read chart dimensions for bounds checking int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Determine pointer direction and flyout X position based on snap state bool ptrLeft; int flyX; if (m_snapState == SNAP_LEFT) { ptrLeft = true; flyX = m_panelX + m_sidebarWidth; } else if (m_snapState == SNAP_RIGHT) { ptrLeft = false; flyX = m_panelX - totalW; } else { //--- For floating panels, prefer opening to the right with fallback to left int rightX = m_panelX + m_sidebarWidth; if (rightX + totalW <= chartW - 4) { ptrLeft = true; flyX = rightX; } else { ptrLeft = false; flyX = m_panelX - totalW; if (flyX < 0) { ptrLeft = true; flyX = rightX; } } } m_flyoutPointerOnLeft = ptrLeft; //--- Compute the flyout Y position aligned to the hovered category button center int btnCentreY = m_panelY + CalcCategoryButtonY((int)cat) + m_categoryButtonSize / 2; int flyY = btnCentreY - (titleH + m_flyoutPadding + 6); //--- Clamp flyout Y within chart bounds if (flyY + flyH > chartH - 8) flyY = chartH - flyH - 8; if (flyY < 4) flyY = 4; //--- Clamp the pointer local Y to stay within the flyout rounded corners m_flyoutPointerLocalY = MathMax(m_panelCornerRadius + m_flyoutPointerWidth + 2, MathMin(flyH - m_panelCornerRadius - m_flyoutPointerWidth - 2, btnCentreY - flyY)); //--- Position the flyout chart object and mark it visible ObjectSetInteger(0, m_nameFlyout, OBJPROP_XDISTANCE, flyX); ObjectSetInteger(0, m_nameFlyout, OBJPROP_YDISTANCE, flyY); m_isFlyoutVisible = true; m_flyoutActiveCat = cat; //--- Draw the flyout contents and make the object visible on all timeframes DrawFlyoutForCategory(cat, activeTool); ObjectSetInteger(0, m_nameFlyout, OBJPROP_TIMEFRAMES, OBJ_ALL_PERIODS); } //+------------------------------------------------------------------+ //| Draw the flyout body border at high resolution | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutBodyBorderHR(int x, int y, int w, int h, int r, int thickness, uint borderColor) { //--- Skip drawing when border width is disabled if (BorderWidth <= 0) return; //--- Clamp corner radius and compute half-thickness offset r = MathMin(r, MathMin(w / 2, h / 2)); int h2 = thickness / 2; //--- Draw all four border edges of the flyout body DrawBorderEdge(m_canvasFlyoutHighRes, x + r, y + h2, x + w - r, y + h2, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + w - h2, y + r, x + w - h2, y + h - r, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + w - r, y + h - h2, x + r, y + h - h2, thickness, borderColor); DrawBorderEdge(m_canvasFlyoutHighRes, x + h2, y + h - r, x + h2, y + r, thickness, borderColor); //--- Draw all four corner arcs of the flyout body DrawCornerArc(m_canvasFlyoutHighRes, x + r, y + r, r, thickness, borderColor, M_PI, M_PI * 1.5); DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + r, r, thickness, borderColor, M_PI * 1.5, M_PI * 2.0); DrawCornerArc(m_canvasFlyoutHighRes, x + r, y + h - r, r, thickness, borderColor, M_PI * 0.5, M_PI); DrawCornerArc(m_canvasFlyoutHighRes, x + w - r, y + h - r, r, thickness, borderColor, 0.0, M_PI * 0.5); } //+------------------------------------------------------------------+ //| Draw and composite the full flyout panel for the given category | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutForCategory(ENUM_CATEGORY cat, TOOL_TYPE activeTool) { //--- Exit early if the category has no tools int nTools = ArraySize(m_categories[(int)cat].tools); if (nTools == 0) return; //--- Compute layout dimensions int titleH = 26, visibleTools = MathMin(nTools, m_flyoutMaxVisibleItems); bool needsScroll = (nTools > m_flyoutMaxVisibleItems); int flyH = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight + m_flyoutPadding; int totalW = m_flyoutWidth + m_flyoutPointerHeight; //--- Compute high-res canvas dimensions int ws = totalW * m_supersampleFactor, hs = flyH * m_supersampleFactor; bool ptrLeft = m_flyoutPointerOnLeft; //--- Compute scroll thumb height if scrolling is needed if (needsScroll) { int trackH = visibleTools * m_flyoutItemHeight; m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (double)m_flyoutMaxVisibleItems / nTools)); } //--- Resize display and high-res canvases if flyout dimensions have changed if (m_canvasFlyout.Width() != totalW || m_canvasFlyout.Height() != flyH) m_canvasFlyout.Resize(totalW, flyH); if (m_canvasFlyoutHighRes.Width() != ws || m_canvasFlyoutHighRes.Height() != hs) m_canvasFlyoutHighRes.Resize(ws, hs); //--- Update the flyout chart object size to match ObjectSetInteger(0, m_nameFlyout, OBJPROP_XSIZE, totalW); ObjectSetInteger(0, m_nameFlyout, OBJPROP_YSIZE, flyH); //--- Clear the high-res canvas to fully transparent m_canvasFlyoutHighRes.Erase(0x00000000); //--- Compute horizontal body offset and width at high resolution int bx = ptrLeft ? m_flyoutPointerHeight * m_supersampleFactor : 0; int bw = m_flyoutWidth * m_supersampleFactor; int br = m_panelCornerRadius * m_supersampleFactor; //--- Compute pointer tip Y and half-height at high resolution int ptrCY = MathMax(br + m_flyoutPointerWidth * m_supersampleFactor + m_supersampleFactor, MathMin(hs - br - m_flyoutPointerWidth * m_supersampleFactor - m_supersampleFactor, m_flyoutPointerLocalY * m_supersampleFactor)); int ptrHHS = m_flyoutPointerWidth * m_supersampleFactor; //--- Compute pointer tip and base X based on pointer direction int tipX = ptrLeft ? 0 : ws - 1, baseX = ptrLeft ? bx : bx + bw - 1; //--- Pack background and border colors uchar flyBgA = (uchar)(255 * BackgroundOpacity); uint fillARGB = ColorToARGB(m_themeColors.flyoutBackground, flyBgA); uint borderARGB = ColorToARGB(m_themeColors.flyoutBorder, 255); int brdT = BorderWidth * m_supersampleFactor; //--- Fill flyout body background with rounded corners FillRoundRectHR(m_canvasFlyoutHighRes, bx, 0, bw, hs, br, fillARGB); //--- Fill the pointer triangle background FillTriangleHR(m_canvasFlyoutHighRes, tipX, ptrCY, baseX, ptrCY - ptrHHS, baseX, ptrCY + ptrHHS, fillARGB); //--- Draw flyout body border and pointer edges if border is enabled if (BorderWidth > 0) { if (ptrLeft) { //--- Draw body border on all four sides DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB); //--- Erase the body-left gap where the pointer connects m_canvasFlyoutHighRes.FillRectangle(bx, ptrCY - ptrHHS, bx + brdT + m_supersampleFactor, ptrCY + ptrHHS, fillARGB); //--- Draw pointer triangle border edges DrawBorderEdge(m_canvasFlyoutHighRes, (double)bx, (double)(ptrCY - ptrHHS), (double)tipX, (double)ptrCY, brdT, borderARGB); DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX, (double)ptrCY, (double)bx, (double)(ptrCY + ptrHHS), brdT, borderARGB); } else { //--- Compute the body right boundary for the right-facing pointer int bodyRight = bx + bw; DrawFlyoutBodyBorderHR(bx, 0, bw, hs, br, brdT, borderARGB); //--- Erase the body-right gap where the pointer connects m_canvasFlyoutHighRes.FillRectangle(bodyRight - brdT - m_supersampleFactor, ptrCY - ptrHHS, bodyRight, ptrCY + ptrHHS, fillARGB); //--- Draw pointer triangle border edges for right-facing pointer DrawBorderEdge(m_canvasFlyoutHighRes, (double)bodyRight, (double)(ptrCY - ptrHHS), (double)tipX, (double)ptrCY, brdT, borderARGB); DrawBorderEdge(m_canvasFlyoutHighRes, (double)tipX, (double)ptrCY, (double)bodyRight, (double)(ptrCY + ptrHHS), brdT, borderARGB); } } //--- Fill the flyout title strip background with rounded top corners color titleFill = m_isDarkTheme ? C'25,29,40' : C'245,247,252'; int tbrd = MathMax(brdT, m_supersampleFactor), innerTR = MathMax(0, br - tbrd); FillSelectiveRoundRectHR(m_canvasFlyoutHighRes, bx + tbrd, tbrd, bw - 2 * tbrd, titleH * m_supersampleFactor - tbrd, innerTR, ColorToARGB(titleFill, 255), true, true, false, false); //--- Square off the lower half of the title strip m_canvasFlyoutHighRes.FillRectangle(bx + tbrd, (titleH / 2) * m_supersampleFactor, bx + bw - tbrd - 1, titleH * m_supersampleFactor - 1, ColorToARGB(titleFill, 255)); //--- Compute item clip boundaries int itemClipTop = titleH + m_flyoutPadding, itemClipBot = titleH + m_flyoutPadding + visibleTools * m_flyoutItemHeight; //--- Draw item highlight backgrounds — use tmpHighRes when scrolling so highlights //--- never bleed above itemClipTop into the title strip after DownsampleCanvas if (needsScroll) { //--- Draw all scrolled item backgrounds onto a temporary HR canvas CCanvas tmpHighRes; tmpHighRes.Create("FlyoutTmpHR", ws, hs, COLOR_FORMAT_ARGB_NORMALIZE); tmpHighRes.Erase(0x00000000); for (int t = 0; t < nTools; t++) { //--- Compute scroll-adjusted item Y at high resolution int itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels) * m_supersampleFactor; //--- Skip items fully above or below the clip region if (itemY + (m_flyoutItemHeight - 2) * m_supersampleFactor <= itemClipTop * m_supersampleFactor) continue; if (itemY >= itemClipBot * m_supersampleFactor) continue; bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); int itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor, padS = m_flyoutPadding * m_supersampleFactor; //--- Fill active item row background onto temp canvas if (isActive) FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Fill hovered item row background onto temp canvas else if (isHovered) FillRoundRectHR(tmpHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255)); //--- Draw active indicator dot onto temp canvas if (isActive) tmpHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor, itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255)); } //--- Blit only the clip region from temp onto the main HR canvas for (int y = itemClipTop * m_supersampleFactor; y < itemClipBot * m_supersampleFactor && y < hs; y++) for (int x = 0; x < ws; x++) { uint px = tmpHighRes.PixelGet(x, y); if (((px >> 24) & 0xFF) > 0) BlendPixelSet(m_canvasFlyoutHighRes, x, y, px); } tmpHighRes.Destroy(); } else { //--- No scrolling — all items fit, draw directly onto the HR canvas for (int t = 0; t < visibleTools; t++) { bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); //--- Compute item Y at high resolution (no scroll offset needed) int itemY = (titleH + m_flyoutPadding + t * m_flyoutItemHeight) * m_supersampleFactor; int itemH = (m_flyoutItemHeight - 2) * m_supersampleFactor; int padS = m_flyoutPadding * m_supersampleFactor; //--- Fill active item row background if (isActive) FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.buttonActiveBackground, 255)); //--- Fill hovered item row background else if (isHovered) FillRoundRectHR(m_canvasFlyoutHighRes, bx + padS, itemY, bw - 2 * padS, itemH, 5 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutItemHoverBackground, 255)); //--- Draw active indicator dot if (isActive) m_canvasFlyoutHighRes.FillCircle(bx + bw - m_flyoutPadding * m_supersampleFactor - 5 * m_supersampleFactor, itemY + itemH / 2, 3 * m_supersampleFactor, ColorToARGB(m_themeColors.flyoutTextActiveColor, 255)); } } //--- Downsample high-res canvas into the display-resolution flyout canvas DownsampleCanvas(m_canvasFlyout, m_canvasFlyoutHighRes, m_supersampleFactor); //--- Compute display-resolution body left offset int dispBx = ptrLeft ? m_flyoutPointerHeight : 0; //--- Draw horizontal separator below the title strip m_canvasFlyout.Line(dispBx + BorderWidth, titleH, dispBx + m_flyoutWidth - BorderWidth - 1, titleH, ColorToARGB(m_themeColors.flyoutBorder, 255)); //--- Draw the uppercased category title text string titleStr = m_categories[(int)cat].categoryLabel; StringToUpper(titleStr); m_canvasFlyout.FontSet("Arial Bold", FlyoutTitleSize); m_canvasFlyout.TextOut(dispBx + m_flyoutPadding + 4, 6, titleStr, ColorToARGB(m_themeColors.flyoutTitleColor, 255)); //--- Draw the tool count badge if the category has more than one tool if (nTools > 1) { string countStr = IntegerToString(nTools); m_canvasFlyout.FontSet("Arial", 15); int cw = m_canvasFlyout.TextWidth(countStr); //--- Right-align the count badge within the title strip m_canvasFlyout.TextOut(dispBx + m_flyoutWidth - m_flyoutPadding - cw - 4, 8, countStr, ColorToARGB(m_themeColors.flyoutTitleColor, 200)); } //--- Create a temporary canvas for the icon and label text pass //--- Drawing directly onto m_canvasFlyout has no Y clipping; TextOut at scrolled //--- positions bleeds into the title strip. tmpText is seeded only for the clip //--- region and blitted back, so glyphs outside [itemClipTop, itemClipBot) are discarded CCanvas tmpText; tmpText.Create("FlyoutTmpText", m_canvasFlyout.Width(), m_canvasFlyout.Height(), COLOR_FORMAT_ARGB_NORMALIZE); tmpText.Erase(0x00000000); //--- Seed the clip region with existing flyout pixels as the drawing background for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++) for (int x = 0; x < m_canvasFlyout.Width(); x++) tmpText.PixelSet(x, y, m_canvasFlyout.PixelGet(x, y)); //--- Draw icon glyphs and label text for each tool row onto the temp canvas for (int t = 0; t < nTools; t++) { //--- Compute scroll-adjusted display-resolution item Y int itemY = titleH + m_flyoutPadding + t * m_flyoutItemHeight - m_flyoutScrollPixels; int itemH = m_flyoutItemHeight - 2; //--- Skip rows fully outside the clip region if (itemY + itemH <= itemClipTop || itemY >= itemClipBot) continue; bool isActive = (activeTool == m_categories[(int)cat].tools[t].toolType); bool isHovered = (m_hoveredFlyoutItem == t && m_flyoutActiveCat == cat); //--- Select icon and text colors based on state color iconColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.buttonIconColor); color textColor = isActive ? m_themeColors.flyoutTextActiveColor : (isHovered ? clrWhite : m_themeColors.flyoutTextColor); //--- Draw tool icon glyph onto the temp canvas tmpText.FontSet(m_categories[(int)cat].tools[t].iconFontName, FlyoutIconSize); string sym = CharToString(m_categories[(int)cat].tools[t].iconCharCode); int ih = tmpText.TextHeight(sym); tmpText.TextOut(dispBx + m_flyoutPadding + 8, itemY + (itemH - ih) / 2, sym, ColorToARGB(iconColor, 255)); //--- Draw tool label text onto the temp canvas tmpText.FontSet("Arial", FlyoutLabelSize); int lh = tmpText.TextHeight(m_categories[(int)cat].tools[t].toolLabel); tmpText.TextOut(dispBx + m_flyoutPadding + 34, itemY + (itemH - lh) / 2, m_categories[(int)cat].tools[t].toolLabel, ColorToARGB(textColor, 255)); } //--- Blit only the clip region back onto the display canvas, discarding any out-of-bounds draws for (int y = itemClipTop; y < itemClipBot && y < m_canvasFlyout.Height(); y++) for (int x = 0; x < m_canvasFlyout.Width(); x++) m_canvasFlyout.PixelSet(x, y, tmpText.PixelGet(x, y)); //--- Destroy the temporary canvas tmpText.Destroy(); //--- Overlay the scroll thumb pill if hover or drag is active DrawFlyoutScrollPillOverlay(cat); //--- Flush the display canvas to the chart m_canvasFlyout.Update(); } //+------------------------------------------------------------------+ //| Draw the flyout scroll thumb pill overlay onto display canvas | //+------------------------------------------------------------------+ void CFlyoutPanel::DrawFlyoutScrollPillOverlay(ENUM_CATEGORY cat) { //--- Skip drawing if neither hovered nor dragging if (!m_isHoveredFlyoutScrollArea && !m_isFlyoutThumbDragging) return; if (cat == CAT_NONE) return; int nTools = ArraySize(m_categories[(int)cat].tools); //--- Skip if all items are visible and no scroll is needed if (nTools <= m_flyoutMaxVisibleItems) return; //--- Compute scroll track geometry int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; m_flyoutScrollThumbHeight = MathMax(20, (int)(trackH * (double)m_flyoutMaxVisibleItems / nTools)); //--- Compute the thumb Y position from the current scroll fraction int maxScrollPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; double scrollPos = (maxScrollPx > 0) ? (double)m_flyoutScrollPixels / maxScrollPx : 0.0; int thumbY = itemsTop + (int)(scrollPos * (trackH - m_flyoutScrollThumbHeight)); //--- Compute scroll pill X position based on pointer direction int tw = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); //--- Select pill color and opacity based on interaction state color pillColor; uchar pillAlpha; if (m_isFlyoutThumbDragging) { pillColor = m_themeColors.accentBarColor; pillAlpha = 255; } else if (m_isHoveredFlyoutThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; } else { pillColor = m_themeColors.scrollArrowColor; pillAlpha = 180; } uint thumbARGB = ColorToARGB(pillColor, pillAlpha); //--- Create a temporary high-res canvas for the pill shape int pws = tw * m_supersampleFactor, phs = m_flyoutScrollThumbHeight * m_supersampleFactor; CCanvas pillHR; pillHR.Create("FlyoutPillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE); pillHR.Erase(0x00000000); //--- Fill the pill with a fully rounded rect at high resolution FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB); //--- Downsample the pill and blend it onto the flyout display canvas for (int py = 0; py < m_flyoutScrollThumbHeight; py++) for (int px = 0; px < tw; px++) { //--- Accumulate channel sums across the high-res sample block double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0; for (int dy = 0; dy < m_supersampleFactor; dy++) for (int dx = 0; dx < m_supersampleFactor; dx++) { int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy; if (sx >= pws || sy >= phs) continue; uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); sumA += a; if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; } } //--- Compute averaged output alpha and blend onto the display canvas int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2); if (fa > 0 && wc > 0) BlendPixelSet(m_canvasFlyout, thinX + px, thumbY + py, ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc)); } //--- Destroy the temporary high-res pill canvas pillHR.Destroy(); }
Сначала мы объявляем класс "CFlyoutPanel", который наследует от "CSidebarLayout" и вводит защищенные члены, охватывающие ширину тела выдвижной панели, высоту строки элементов, отступы, размеры и направление треугольника указателя, флаг видимости, активную категорию, индекс элемента при наведении курсора, смещение прокрутки, максимальное количество видимых элементов, высоту ползунка прокрутки, состояние перетаскивания ползунка и флаги при наведении курсора на прокрутку. Мы также объявляем семь защищенных методов для отображения, скрытия, рисования, отрисовки ползунка прокрутки, рисования границ и две функции проверки попадания.
Далее метод "HideFlyout" сбрасывает значение элемента при наведении курсора и смещение прокрутки, очищает флаги при наведении курсора на прокрутку, скрывает объект графика выдвижной панели, устанавливая его временные рамки в OBJ_NO_PERIODS, и помечает всплывающее окно как скрытое. Метод "HitTestOverFlyout" считывает положение и размер объекта графика выдвижной панели с помощью ObjectGetInteger, вычисляет локальные координаты и возвращает значение, указывающее, находится ли курсор мыши в пределах его границ. Метод "HitTestFlyoutItem" вычисляет, над каким рядом инструментов находится курсор, учитывая высоту заголовка, отступы, смещение прокрутки и границы клипов, исключая при этом щелчки по столбцу с ползуном прокрутки.
Метод "ShowFlyout" обрабатывает всю логику позиционирования. Мы вычисляем высоту выдвижной панели на основе количества видимых инструментов, определяем направление указателя на основе состояния привязки с резервным вариантом справа для плавающих панелей, выравниваем выдвижную панель по вертикали относительно центра кнопки категории, на которую наведен курсор, ограничиваем его пределами графика, располагаем кончик указателя внутри закругленных углов выдвижной панели, устанавливаем положение объекта графика и вызываем метод рисования для отображения содержимого.
Метод "DrawFlyoutBodyBorderHR" рисует границу выдвижной панели с высоким разрешением, используя четыре сегмента ребер и четыре угловые дуги на холсте выдвижной панели с высоким разрешением. Метод "DrawFlyoutForCategory" является основным конвейером рендеринга. Мы вычисляем размеры раскладки, при необходимости изменяем размер холстов, очищаем холст высокого разрешения, заполняем фон основного текста с закругленными углами с помощью функции "FillRoundRectHR", рисуем треугольник указателя с помощью функции "FillTriangleHR" и отображаем границу с зазором, стертым в месте соединения указателя. Мы заполняем полосу заголовка с закругленными верхними углами, затем рисуем подсветку строк элементов для активного и наведенного состояний, используя временный обрезанный холст при прокрутке, чтобы предотвратить распространение подсветки в область заголовка. После уменьшения разрешения мы рисуем разделительную линию, текст заголовка в верхнем регистре, значок количества инструментов, а также иконку и метку каждого инструмента на временном текстовом холсте, который затем копируется обратно в область обрезки, чтобы предотвратить переполнение текста. Наконец, мы накладываем ползунок прокрутки и выравниваем результат.
Метод "DrawFlyoutScrollPillOverlay" отображает тонкий закругленный элемент в виде капсулы на краю выдвижной панели при наведении курсора на область прокрутки или при перетаскивании ползунка. Мы вычисляем положение ползунка на основе доли прокрутки, создаем временный холст высокого разрешения, заполняем его скругленным прямоугольником, вручную уменьшаем разрешение и накладываем результат на холст выдвижной панели. При этом цвет и прозрачность меняются в зависимости от того, находится ли ползунок в состоянии покоя, под курсором или активно перетаскивается. После рендеринга мы получим следующее наложение.

После этого мы расширим боковую панель, добавив состояния при наведении курсора и ползунок полосы прокрутки.
Расширение рендерера боковой панели с помощью состояний наведения курсора и ползунка прокрутки
Класс рендерера теперь наследует от выдвижной панели и получает отслеживание при наведении курсора, распознавание активных инструментов и наложение ползунка прокрутки на боковую панель.
//+------------------------------------------------------------------+ //| CLASS 7 — Draw and composite all sidebar visual elements | //+------------------------------------------------------------------+ class CSidebarRenderer : public CFlyoutPanel { protected: ENUM_CATEGORY m_hoveredCategory; // Currently hovered category button, or CAT_NONE bool m_isCloseButtonHovered; // Flag indicating the close button is hovered bool m_isThemeButtonHovered; // Flag indicating the theme toggle button is hovered bool m_isGripAreaHovered; // Flag indicating the drag-grip area is hovered protected: //--- Draw and composite the full sidebar onto its canvas void DrawSidebar(TOOL_TYPE activeTool); //--- Draw the header grip strip at high resolution void DrawHeaderStripHR(int canvasW, int canvasH); //--- Draw a single category button with active, hover, and dot states at high resolution void DrawCategoryButtonHR(CCanvas &target, int xHR, int yHR, int sizeHR, bool isActive, bool isHovered, bool hasDot); //--- Draw icon glyphs and separator lines onto the display canvas void DrawSidebarIconLabels(TOOL_TYPE activeTool); //--- Draw the sidebar scroll thumb pill overlay onto the display canvas void DrawSidebarScrollPillOverlay(); }; //+------------------------------------------------------------------+ //| Draw the sidebar scroll thumb pill overlay onto display canvas | //+------------------------------------------------------------------+ void CSidebarRenderer::DrawSidebarScrollPillOverlay() { //--- Skip drawing if scrolling is not needed or neither hovered nor dragging if (CalcSidebarMaxScrollPixels() <= 0 || (!m_isHoveredSidebarScrollArea && !m_isSidebarThumbDragging)) return; //--- Compute scroll track geometry int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_sidebarScrollThumbHeight = MathMax(20, (int)(trackH * (double)trackH / CalcSidebarTotalScrollPixels())); int maxPx = CalcSidebarMaxScrollPixels(); //--- Compute the thumb Y position from the current scroll fraction double pos = (maxPx > 0) ? (double)m_sidebarScrollPixels / maxPx : 0.0; int thumbY = trackY + (int)(pos * (trackH - m_sidebarScrollThumbHeight)); //--- Compute scroll pill X position based on snap state int tw = m_sidebarScrollThinWidth; int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; //--- Select pill color and opacity based on interaction state color pillColor; uchar pillAlpha; if (m_isSidebarThumbDragging) { pillColor = m_themeColors.accentBarColor; pillAlpha = 255; } else if (m_isHoveredSidebarThumb) { pillColor = m_themeColors.scrollArrowHoverColor; pillAlpha = 255; } else { pillColor = m_themeColors.scrollArrowColor; pillAlpha = 180; } uint thumbARGB = ColorToARGB(pillColor, pillAlpha); //--- Create a temporary high-res canvas for the pill shape int pws = tw * m_supersampleFactor, phs = m_sidebarScrollThumbHeight * m_supersampleFactor; CCanvas pillHR; pillHR.Create("SB_PillHR_tmp", pws, phs, COLOR_FORMAT_ARGB_NORMALIZE); pillHR.Erase(0x00000000); //--- Fill the pill with a fully rounded rect at high resolution FillRoundRectHR(pillHR, 0, 0, pws, phs, MathMax(1, pws / 2), thumbARGB); //--- Downsample the pill and blend it onto the sidebar display canvas for (int py = 0; py < m_sidebarScrollThumbHeight; py++) for (int px = 0; px < tw; px++) { //--- Accumulate channel sums across the high-res sample block double sumA = 0, sumR = 0, sumG = 0, sumB = 0, wc = 0; for (int dy = 0; dy < m_supersampleFactor; dy++) for (int dx = 0; dx < m_supersampleFactor; dx++) { int sx = px * m_supersampleFactor + dx, sy = py * m_supersampleFactor + dy; if (sx >= pws || sy >= phs) continue; uint p = pillHR.PixelGet(sx, sy); uchar a = (uchar)((p >> 24) & 0xFF); sumA += a; if (a > 0) { sumR += (p >> 16) & 0xFF; sumG += (p >> 8) & 0xFF; sumB += p & 0xFF; wc += 1.0; } } //--- Compute averaged output alpha and blend onto the display canvas int ss2 = m_supersampleFactor * m_supersampleFactor; uchar fa = (uchar)(sumA / ss2); if (fa > 0 && wc > 0) BlendPixelSet(m_canvasSidebar, thinX + px, thumbY + py, ((uint)fa << 24) | ((uint)(uchar)(sumR / wc) << 16) | ((uint)(uchar)(sumG / wc) << 8) | (uint)(uchar)(sumB / wc)); } //--- Destroy the temporary high-res pill canvas pillHR.Destroy(); }
Здесь мы объявляем класс "CSidebarRenderer", который теперь наследует от "CFlyoutPanel" вместо "CSidebarLayout", как в предыдущей части, предоставляя ему доступ ко всей системе выдвижных панелей. Мы добавляем четыре новых защищенных члена, отслеживающих, какая кнопка категории находится под курсором и находится ли кнопка закрытия, кнопка темы или область захвата в данный момент под курсором. Метод "DrawSidebar" теперь принимает в качестве параметра тип активного инструмента, что позволяет определить, какую кнопку категории следует выделить. Метод "DrawCategoryButtonHR" получает логические параметры active и hovered в дополнение к существующему флагу dot, что позволяет отображать три визуальных состояния. Мы также добавляем метод "DrawSidebarScrollPillOverlay" и сохраняем существующие методы отрисовки заголовка и меток значков, которые теперь принимают тип активного инструмента для отрисовки с учетом состояния.
Метод "DrawSidebarScrollPillOverlay" отображает тонкий закругленный индикатор прокрутки на краю боковой панели, когда список категорий выходит за пределы области прокрутки и пользователь наводит курсор или перетаскивает область прокрутки. Мы вычисляем положение ползунка относительно текущей доли прокрутки и высоты дорожки, позиционируем его на левом или правом краю в зависимости от состояния привязки и выбираем цвет ползунка в зависимости от того, находится ли ползунок в состоянии покоя, под курсором или перетаскивается. Далее мы создаём временный холст высокого разрешения, заполняем его скруглённым прямоугольником, вручную уменьшаем разрешение пиксель за пикселем и накладываем полученный результат на холст боковой панели с помощью функции "BlendPixelSet". Это придаёт ползунку прокрутки такое же сглаживание, как и остальной части боковой панели. После этого у нас есть все необходимые элементы. Теперь мы определим новый класс для обработки всех событий мыши и графиков. Сначала давайте объявим его.
Представляем класс обработчика событий графика
Этот совершенно новый класс служит центральным маршрутным узлом для всех событий взаимодействия с мышью, клавиатурой и графиками, преобразуя действия пользователя в ответы для боковой панели, выдвижной панели и графических элементов.
//+------------------------------------------------------------------+ //| CLASS 8 — Route and handle all chart interaction events | //+------------------------------------------------------------------+ class CChartEventHandler : public CSidebarRenderer { protected: int m_previousMouseButtonState; // Mouse button state recorded on the previous move event protected: //--- Dispatch an incoming chart event to the appropriate handler void RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool); //--- Handle CHARTEVENT_CHART_CHANGE to reflow layout on resize void OnChartChangeEvent(TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_WHEEL to scroll sidebar or flyout void OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool); //--- Handle CHARTEVENT_MOUSE_MOVE to process all mouse interactions void OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool); //--- Move the panel to follow the mouse during a drag operation void HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalize a panel drag by snapping to the nearest edge void HandlePanelDragRelease(TOOL_TYPE activeTool); //--- Resize the panel bottom edge as the mouse moves void HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Scroll the sidebar by moving the scroll thumb void HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool); //--- Finalize a sidebar scroll thumb drag void HandleSidebarThumbRelease(TOOL_TYPE activeTool); //--- Scroll the flyout list by moving the flyout scroll thumb void HandleFlyoutThumbDrag(int mouseX, int mouseY); //--- Finalize a flyout scroll thumb drag void HandleFlyoutThumbRelease(); //--- Recompute all hover state flags and trigger redraws as needed void UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool); //--- Handle a mouse button-down event within the sidebar or flyout void HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool); };
Здесь мы объявляем класс "CChartEventHandler", который наследует от "CSidebarRenderer" и вводит единственную переменную состояния, отслеживающую предыдущее состояние кнопки мыши для обнаружения новых переходов при щелчке. Класс объявляет четырнадцать защищенных методов, которые в совокупности обрабатывают все взаимодействия пользователя, поддерживаемые боковой панелью.
Метод "RouteChartEvent" будет действовать как диспетчер верхнего уровня, перенаправляя события изменения графика в "OnChartChangeEvent" для переформатирования компоновки при изменении размера окна, события прокрутки колесика мыши в "OnMouseWheelEvent" для прокрутки боковой панели или выдвижной панели и события перемещения мыши в "OnMouseMoveEvent" для полного конвейера взаимодействия. Обработчик перемещения мыши является наиболее сложным, обрабатывая сначала активные операции перетаскивания и ползунка, прежде чем перейти к обновлениям состояния при наведении курсора и обнаружению новых щелчков.
Для перемещения панели функция "HandlePanelDragMove" зафиксирует положение панели в пределах границ графика и переместит выдвижную панель вслед за ним, а функция "HandlePanelDragRelease" завершит перетаскивание, вызвав логику привязки к краю и пересчитав компоновку. Метод "HandleBottomResizeDrag" будет регулировать высоту панели по мере перемещения мыши, ограничивая ее диапазоном между минимальным и естественным значениями. Для взаимодействия с прокруткой методы "HandleSidebarThumbDrag" и "HandleFlyoutThumbDrag" сопоставляют изменения положения мыши со смещениями прокрутки для соответствующих списков, с соответствующими методами отпускания, которые очищают состояние перетаскивания.
Метод "UpdateAllHoverStates" сохраняет текущее состояние флагов наведения и затем очищает их, а затем пересчитывает их из текущего положения мыши с помощью проверок нажатия мыши. Он отображает или скрывает выдвижную панель по мере необходимости и перерисовывает его только при изменении состояния. Метод "HandleMouseClickDown" обрабатывает новые нажатия левой кнопки мыши, перенаправляя их на перетаскивание ползунком по ленте, прокрутку страниц, инициирование перетаскивания области захвата, инициирование изменения размера нижней части экрана или удаление кнопки закрытия, в зависимости от того, над чем находится курсор мыши. Давайте теперь подробно рассмотрим все эти методы.
//+------------------------------------------------------------------+ //| Dispatch an incoming chart event to the appropriate handler | //+------------------------------------------------------------------+ void CChartEventHandler::RouteChartEvent(const int id, const long &lp, const double &dp, const string &sp, TOOL_TYPE &activeTool) { //--- Forward chart change events to the resize/reflow handler if (id == CHARTEVENT_CHART_CHANGE) { OnChartChangeEvent(activeTool); return; } //--- Forward mouse wheel events to the scroll handler if (id == CHARTEVENT_MOUSE_WHEEL) { OnMouseWheelEvent((int)(short)lp, (int)(short)(lp >> 16), (int)dp, activeTool); return; } //--- Forward mouse move events to the full mouse interaction handler if (id == CHARTEVENT_MOUSE_MOVE) OnMouseMoveEvent((int)lp, (int)dp, (int)sp, activeTool); } //+------------------------------------------------------------------+ //| Handle chart change event to reflow layout on resize | //+------------------------------------------------------------------+ void CChartEventHandler::OnChartChangeEvent(TOOL_TYPE activeTool) { //--- Reset all drag and thumb states on chart geometry change m_previousMouseButtonState = 0; m_isPanelDragging = false; m_isResizingBottomEdge = false; m_isSidebarThumbDragging = false; m_isFlyoutThumbDragging = false; //--- Restore chart mouse scroll in case it was locked during a drag ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Reposition and reflow snapped panels only if (m_snapState != SNAP_FLOAT) { //--- Recalculate snapped panel X position from chart width int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); m_panelX = (m_snapState == SNAP_RIGHT) ? chartW - m_sidebarWidth : 0; ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); //--- Clamp snapped height override within the new chart height if (m_snappedSidebarHeight > 0) m_snappedSidebarHeight = MathMin(m_snappedSidebarHeight, (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS) - m_panelY - 8); //--- Recompute layout and resize canvases CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); //--- Reposition the flyout if it is currently visible if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); } ChartRedraw(); } //+------------------------------------------------------------------+ //| Handle mouse wheel event to scroll sidebar or flyout list | //+------------------------------------------------------------------+ void CChartEventHandler::OnMouseWheelEvent(int mouseX, int mouseY, int wheelDelta, TOOL_TYPE activeTool) { int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Scroll the sidebar when the wheel is over the sidebar and it is scrollable if (overSidebar && m_sidebarMaxVisibleCats < CAT_COUNT) { //--- Lock chart scroll to prevent chart panning while scrolling the sidebar ChartSetInteger(0, CHART_MOUSE_SCROLL, false); int step = MathMax(1, MouseScrollSpeed); //--- Scroll down on negative delta, up on positive m_sidebarScrollPixels = MathMax(0, MathMin(m_sidebarScrollPixels + ((wheelDelta < 0) ? step : -step), CalcSidebarMaxScrollPixels())); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); return; } //--- Scroll the flyout list when the wheel is over the flyout and it is scrollable if (overFlyout && m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools > m_flyoutMaxVisibleItems) { //--- Lock chart scroll to prevent chart panning while scrolling the flyout ChartSetInteger(0, CHART_MOUSE_SCROLL, false); int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; //--- Scroll flyout list by the configured step amount m_flyoutScrollPixels = MathMax(0, MathMin(m_flyoutScrollPixels + ((wheelDelta < 0) ? MathMax(1, MouseScrollSpeed) : -MathMax(1, MouseScrollSpeed)), maxPx)); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } return; } //--- Restore chart scroll when the wheel is not over any panel ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Move the panel to follow the mouse during a drag operation | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragMove(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Clamp panel position within chart bounds int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS), chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); m_panelX = MathMax(0, MathMin(chartW - m_sidebarWidth, mouseX - m_dragOffsetX)); m_panelY = MathMax(0, MathMin(chartH - m_sidebarHeight, mouseY - m_dragOffsetY)); //--- Update the chart object position to follow the mouse ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Reposition the flyout to follow the sidebar if visible if (m_isFlyoutVisible) ShowFlyout(m_flyoutActiveCat, activeTool); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Finalize panel drag by snapping to the nearest edge | //+------------------------------------------------------------------+ void CChartEventHandler::HandlePanelDragRelease(TOOL_TYPE activeTool) { //--- Clear drag state flag m_isPanelDragging = false; //--- Attempt to snap the panel to a chart edge TrySnapToEdge(); //--- Recompute layout after potential snap state change CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Resize the panel bottom edge as the mouse moves | //+------------------------------------------------------------------+ void CChartEventHandler::HandleBottomResizeDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Compute vertical mouse delta from drag start int dy = mouseY - m_bottomResizeDragStartY; int chartH = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Compute natural height for all buttons and height clamp limits int naturalH = m_headerGripHeight + 8 + CAT_COUNT * (m_categoryButtonSize + 6) - 6 + 10; int minH = m_headerGripHeight + 8 + 10 + 3 * (m_categoryButtonSize + 6) - 6; int maxH = (m_snapState != SNAP_FLOAT) ? MathMin(naturalH, chartH - m_panelY - 8) : chartH - m_panelY - 8; //--- Compute the new panel height clamped within valid bounds int newH = MathMax(minH, MathMin(maxH, m_bottomResizeStartHeight + dy)); //--- Apply the new height if it has changed if (newH != m_sidebarHeight) { //--- Store as snapped height override for non-floating panels if (m_snapState != SNAP_FLOAT) m_snappedSidebarHeight = newH; else m_sidebarHeight = newH; CalcSidebarHeight(); ResizeSidebarCanvases(m_sidebarWidth, m_sidebarHeight); DrawSidebar(activeTool); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Scroll the sidebar by moving the scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbDrag(int mouseX, int mouseY, TOOL_TYPE activeTool) { //--- Compute the available travel distance for the thumb int trackH = CalcSidebarViewportPixels(), travel = trackH - m_sidebarScrollThumbHeight; if (travel > 0) { //--- Map mouse delta to scroll offset delta int dy = mouseY - m_sidebarThumbDragStartY; int maxPx = CalcSidebarMaxScrollPixels(); int newPx = MathMax(0, MathMin(maxPx, m_sidebarThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); //--- Apply the new scroll offset if it has changed if (newPx != m_sidebarScrollPixels) { m_sidebarScrollPixels = newPx; HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalize a sidebar scroll thumb drag | //+------------------------------------------------------------------+ void CChartEventHandler::HandleSidebarThumbRelease(TOOL_TYPE activeTool) { //--- Clear the sidebar thumb dragging flag and redraw m_isSidebarThumbDragging = false; DrawSidebar(activeTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Scroll the flyout list by moving the flyout scroll thumb | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbDrag(int mouseX, int mouseY) { //--- Exit if no flyout category is active if (m_flyoutActiveCat == CAT_NONE) return; int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools <= m_flyoutMaxVisibleItems) return; //--- Compute the available travel distance for the flyout thumb int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight, travel = trackH - m_flyoutScrollThumbHeight; if (travel > 0) { //--- Map mouse delta to flyout scroll offset delta int dy = mouseY - m_flyoutThumbDragStartY; int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int newPx = MathMax(0, MathMin(maxPx, m_flyoutThumbDragStartPixels + (int)MathRound((double)dy / travel * maxPx))); //--- Apply the new flyout scroll offset if it has changed if (newPx != m_flyoutScrollPixels) { m_flyoutScrollPixels = newPx; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } } } //+------------------------------------------------------------------+ //| Finalize a flyout scroll thumb drag | //+------------------------------------------------------------------+ void CChartEventHandler::HandleFlyoutThumbRelease() { //--- Clear the flyout thumb dragging flag and redraw m_isFlyoutThumbDragging = false; DrawFlyoutForCategory(m_flyoutActiveCat, TOOL_NONE); ChartRedraw(); } //+------------------------------------------------------------------+ //| Recompute all hover states and trigger redraws as needed | //+------------------------------------------------------------------+ void CChartEventHandler::UpdateAllHoverStates(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE activeTool) { //--- Snapshot all hover flags before updating for change detection ENUM_CATEGORY prevHovCat = m_hoveredCategory; int prevHovItem = m_hoveredFlyoutItem; bool prevClose = m_isCloseButtonHovered, prevTheme = m_isThemeButtonHovered; bool prevGrip = m_isGripAreaHovered, prevSBA = m_isHoveredSidebarScrollArea; bool prevFSA = m_isHoveredFlyoutScrollArea, prevBR = m_isBottomResizeHovered; bool prevSbTh = m_isHoveredSidebarThumb, prevFlyTh = m_isHoveredFlyoutThumb; //--- Clear all hover flags before recomputing m_isCloseButtonHovered = m_isThemeButtonHovered = m_isGripAreaHovered = false; m_isBottomResizeHovered = m_isHoveredSidebarScrollArea = m_isHoveredSidebarThumb = false; m_isHoveredFlyoutScrollArea = m_isHoveredFlyoutThumb = false; //--- Recompute sidebar hover states when the mouse is over the sidebar if (overSidebar) { m_hoveredCategory = HitTestCategoryButton(lx, ly); m_isCloseButtonHovered = HitTestOverCloseButton(lx, ly); m_isThemeButtonHovered = HitTestOverThemeButton(lx, ly); m_isGripAreaHovered = HitTestOverGripArea(lx, ly); m_isBottomResizeHovered = HitTestOverBottomResizeGrip(lx, ly); //--- Recompute scroll thumb hover state if the sidebar is scrollable if (CalcSidebarMaxScrollPixels() > 0) { int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(); m_isHoveredSidebarScrollArea = (ly >= trackY && ly <= trackY + trackH); if (m_isHoveredSidebarScrollArea) { //--- Check if the mouse is over the narrow scroll pill column int tw = m_sidebarScrollThinWidth, thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; if (lx >= thinX - 4 && lx <= thinX + tw + 4) { //--- Compute thumb Y position from scroll fraction and check hit int maxPx = CalcSidebarMaxScrollPixels(); int sliderY = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight)); m_isHoveredSidebarThumb = (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight); } } } } //--- Clear hovered category if not over the flyout either else if (!overFlyout) m_hoveredCategory = CAT_NONE; //--- Recompute flyout hover states when the mouse is over the flyout if (overFlyout) { m_hoveredFlyoutItem = HitTestFlyoutItem(flx, fly); if (m_hoveredFlyoutItem < 0) m_hoveredFlyoutItem = -1; //--- Mark flyout scroll area as hovered if it is scrollable m_isHoveredFlyoutScrollArea = m_isFlyoutVisible && m_flyoutActiveCat != CAT_NONE && (ArraySize(m_categories[(int)m_flyoutActiveCat].tools) > m_flyoutMaxVisibleItems); if (m_isHoveredFlyoutScrollArea) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int tw = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; //--- Compute the scroll pill X column based on pointer direction int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH) { //--- Compute flyout thumb Y position from scroll fraction and check hit int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight)); m_isHoveredFlyoutThumb = (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight); } } } //--- Clear flyout item hover when not over either panel else if (!overSidebar) { m_hoveredFlyoutItem = -1; m_isHoveredFlyoutScrollArea = false; } //--- Show the flyout when hovering a new category button if (overSidebar && m_hoveredCategory != CAT_NONE && !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered) { if (m_hoveredCategory != m_flyoutActiveCat) ShowFlyout(m_hoveredCategory, activeTool); } //--- Hide the flyout when the mouse leaves both panels else if (!overFlyout && m_isFlyoutVisible) { //--- Allow brief transition across the gap between sidebar and flyout bool transitEdge = false; if (overSidebar) { int margin = m_sidebarWidth / 4; transitEdge = (m_snapState == SNAP_LEFT) ? (lx >= m_sidebarWidth - margin) : (m_snapState == SNAP_RIGHT) ? (lx <= margin) : (m_flyoutPointerOnLeft ? (lx >= m_sidebarWidth - margin) : (lx <= margin)); } if (!transitEdge) { HideFlyout(); ChartRedraw(); } } //--- Trigger a redraw only when any hover state has changed bool changed = (prevHovCat != m_hoveredCategory || prevHovItem != m_hoveredFlyoutItem || prevClose != m_isCloseButtonHovered || prevTheme != m_isThemeButtonHovered || prevGrip != m_isGripAreaHovered || prevSBA != m_isHoveredSidebarScrollArea || prevFSA != m_isHoveredFlyoutScrollArea || prevBR != m_isBottomResizeHovered || prevSbTh != m_isHoveredSidebarThumb || prevFlyTh != m_isHoveredFlyoutThumb); if (changed) { DrawSidebar(activeTool); if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } } //+------------------------------------------------------------------+ //| Handle a mouse button-down event within the sidebar or flyout | //+------------------------------------------------------------------+ void CChartEventHandler::HandleMouseClickDown(int mouseX, int mouseY, bool overSidebar, bool overFlyout, int lx, int ly, int flx, int fly, TOOL_TYPE &activeTool) { //--- Handle sidebar scroll thumb and track clicks if (overSidebar && CalcSidebarMaxScrollPixels() > 0) { int trackY = CalcClipTop(), trackH = CalcSidebarViewportPixels(), tw = m_sidebarScrollThinWidth; int thinX = (m_snapState == SNAP_RIGHT) ? 2 : m_sidebarWidth - tw - 2; if (lx >= thinX - 4 && lx <= thinX + tw + 4 && ly >= trackY && ly <= trackY + trackH) { int maxPx = CalcSidebarMaxScrollPixels(); int sliderY = trackY + (int)((maxPx > 0 ? (double)m_sidebarScrollPixels / maxPx : 0.0) * (trackH - m_sidebarScrollThumbHeight)); //--- Begin thumb drag if the click is on the thumb if (ly >= sliderY && ly <= sliderY + m_sidebarScrollThumbHeight) { m_isSidebarThumbDragging = true; m_sidebarThumbDragStartY = mouseY; m_sidebarThumbDragStartPixels = m_sidebarScrollPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } else { //--- Page-scroll the sidebar by one button step when clicking the track int step = m_categoryButtonSize + m_categoryButtonPadding; m_sidebarScrollPixels = MathMax(0, MathMin(maxPx, m_sidebarScrollPixels + ((ly < sliderY) ? -step : step))); HideFlyout(); DrawSidebar(activeTool); ChartRedraw(); } return; } } //--- Handle flyout scroll thumb and track clicks if (overFlyout && m_flyoutActiveCat != CAT_NONE) { int nTools = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (nTools > m_flyoutMaxVisibleItems) { int titleH = 26, itemsTop = titleH + m_flyoutPadding; int trackH = MathMin(nTools, m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int tw = m_sidebarScrollThinWidth, dispBx = m_flyoutPointerOnLeft ? m_flyoutPointerHeight : 0; int thinX = m_flyoutPointerOnLeft ? (dispBx + m_flyoutWidth - tw - 2) : (dispBx + 2); if (flx >= thinX - 6 && flx <= thinX + tw + 6 && fly >= itemsTop && fly <= itemsTop + trackH) { int maxPx = (nTools - m_flyoutMaxVisibleItems) * m_flyoutItemHeight; int sliderY = itemsTop + (int)((maxPx > 0 ? (double)m_flyoutScrollPixels / maxPx : 0.0) * (trackH - m_flyoutScrollThumbHeight)); //--- Begin flyout thumb drag if the click is on the thumb if (fly >= sliderY && fly <= sliderY + m_flyoutScrollThumbHeight) { m_isFlyoutThumbDragging = true; m_flyoutThumbDragStartY = mouseY; m_flyoutThumbDragStartPixels = m_flyoutScrollPixels; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } else { //--- Page-scroll the flyout by one item height when clicking the track m_flyoutScrollPixels = MathMax(0, MathMin(maxPx, m_flyoutScrollPixels + ((fly < sliderY) ? -m_flyoutItemHeight : m_flyoutItemHeight))); DrawFlyoutForCategory(m_flyoutActiveCat, activeTool); ChartRedraw(); } return; } } } //--- Begin panel drag when clicking the grip area if (overSidebar && HitTestOverGripArea(lx, ly) && !m_isCloseButtonHovered && !m_isThemeButtonHovered) { m_isPanelDragging = true; m_dragOffsetX = lx; m_dragOffsetY = ly; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); return; } //--- Begin bottom resize drag when clicking the resize grip if (overSidebar && HitTestOverBottomResizeGrip(lx, ly)) { m_isResizingBottomEdge = true; m_bottomResizeDragStartY = mouseY; m_bottomResizeStartHeight = m_sidebarHeight; ChartSetInteger(0, CHART_MOUSE_SCROLL, false); HideFlyout(); return; } //--- Remove the indicator when the close button is clicked if (overSidebar && m_isCloseButtonHovered) { ExpertRemove(); return; } } //+------------------------------------------------------------------+ //| Handle CHARTEVENT_MOUSE_MOVE to process all mouse interactions | //+------------------------------------------------------------------+ void CChartEventHandler::OnMouseMoveEvent(int mouseX, int mouseY, int mouseButtons, TOOL_TYPE &activeTool) { int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Handle active drag and thumb operations first, before hover or click logic if (m_isPanelDragging && mouseButtons == 1) { HandlePanelDragMove(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isPanelDragging && mouseButtons == 0) { HandlePanelDragRelease(activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isResizingBottomEdge && mouseButtons == 1) { HandleBottomResizeDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isResizingBottomEdge && mouseButtons == 0) { m_isResizingBottomEdge = false; m_previousMouseButtonState = mouseButtons; return; } if (m_isSidebarThumbDragging && mouseButtons == 1) { HandleSidebarThumbDrag(mouseX, mouseY, activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isSidebarThumbDragging && mouseButtons == 0) { HandleSidebarThumbRelease(activeTool); m_previousMouseButtonState = mouseButtons; return; } if (m_isFlyoutThumbDragging && mouseButtons == 1) { HandleFlyoutThumbDrag(mouseX, mouseY); m_previousMouseButtonState = mouseButtons; return; } if (m_isFlyoutThumbDragging && mouseButtons == 0) { HandleFlyoutThumbRelease(); m_previousMouseButtonState = mouseButtons; return; } //--- Recompute all hover states for the current mouse position UpdateAllHoverStates(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); //--- Manage chart scroll lock: lock when over any panel, unlock otherwise bool overAny = overSidebar || overFlyout; if (!m_isSidebarThumbDragging && !m_isPanelDragging && !m_isResizingBottomEdge && !m_isFlyoutThumbDragging) ChartSetInteger(0, CHART_MOUSE_SCROLL, !overAny); //--- Detect a fresh left-button press and route to the click-down handler if (mouseButtons == 1 && m_previousMouseButtonState == 0) HandleMouseClickDown(mouseX, mouseY, overSidebar, overFlyout, lx, ly, flx, fly, activeTool); //--- Record button state for next event comparison m_previousMouseButtonState = mouseButtons; }
Сначала реализуем метод "RouteChartEvent" в качестве диспетчера верхнего уровня. Он проверяет идентификатор события и перенаправляет события изменения графика в метод "OnChartChangeEvent", события прокрутки колесом мыши в метод "OnMouseWheelEvent" с извлеченными координатами и дельтой, а события перемещения мыши в метод "OnMouseMoveEvent".
Затем метод "OnChartChangeEvent" сбрасывает все состояния перетаскивания и ползунка, восстанавливает прокрутку графика. Далее перестраивает макет для привязанных панелей, пересчитывает положение панели из ширины графика, ограничивает переопределение привязанной высоты, пересчитывает высоту боковой панели, изменяет размер холстов, перерисовывает и перепозиционирует выдвижную панель, если она видна.
Метод "OnMouseWheelEvent" проверяет попадание мыши на боковую панель и выдвижную панель. Когда колесико мыши находится над боковой панелью и происходит переполнение категорий, мы блокируем прокрутку графика и корректируем смещение прокрутки боковой панели на заданный шаг. Когда курсор находится над выдвижной панелью с возможностью прокрутки мы вместо этого регулируем смещение прокрутки. Если курсор не находится ни над одной из панелей, мы восстанавливаем прокрутку графика.
Для перемещения панелей "HandlePanelDragMove" ограничивает положение панели границами графика с помощью MathMax и MathMin, обновляет положение объекта графика, перемещает выдвижную панель, если она видима, и перерисовывает его. "HandlePanelDragRelease" сбрасывает флаг перетаскивания, вызывает "TrySnapToEdge", пересчитывает компоновку и изменяет размер холстов. "HandleBottomResizeDrag" вычисляет разницу от начала перетаскивания, ограничивает новую высоту между минимальным и естественным пределами, сохраняет ее как переопределение высоты для закрепленных панелей и перестраивает компоновку.
Методы перетаскивания боковой панели и выдвижной панели с помощью ползунка сопоставляют дельты мыши с изменениями смещения прокрутки пропорционально доступному расстоянию перемещения, скрывая выдвижную панель при прокрутке боковой панели и перерисовывая его после каждого обновления. Соответствующие методы освобождения сбрасывают флаг перетаскивания и запускают окончательную перерисовку.
Метод "UpdateAllHoverStates" делает снимки всех флагов наведения курсора, очищает их, пересчитывает каждый из них, исходя из текущего положения мыши, используя методы проверки попадания, проверяет, находится ли ползунок прокрутки под курсором, вычисляя его положение на основе доли прокрутки, отображает выдвижную панель при наведении курсора на новую категорию, скрывает его, когда курсор мыши покидает обе панели с учетом допуска на переход, и запускает перерисовку только при изменении состояния.
Метод "HandleMouseClickDown" обрабатывает новые нажатия левой кнопки мыши, сначала проверяя столбцы прокрутки боковой панели и выдвижной панели на предмет перетаскивания ползунка или отслеживания прокрутки страниц. Затем инициирует перетаскивание панели из области захвата, изменение размера нижней части из области изменения размера или вызывает ExpertRemove при нажатии кнопки закрытия.
Метод "OnMouseMoveEvent" объединяет все это воедино. Он проверяет попадание на обе панели, обрабатывает сначала активные операции перетаскивания и нажатия ползунка с ранним возвратом, переходит к обновлениям состояния при наведении курсора, управляет блокировкой прокрутки графика при наведении на любую панель, обнаруживает новые щелчки и записывает состояние кнопки для следующего события. Это обеспечивает нам интерактивность, о которой мы так долго мечтали. См. пример иллюстрации ниже.

После этого мы можем приступить к разработке фактического рисунка на графике. Мы выбрали и активировали инструмент, так что же дальше? Нам нужно убедиться, что мы используем именно этот инструмент, который позволит нам достичь поставленных целей. Для этого мы создадим новый класс.
Представляем класс графического движка
Этот совершенно новый класс преобразует выбор инструментов в реальные объекты графика, управляет сбором якорных точек за несколько кликов и направляет их к нужному методу создания объекта.
//+------------------------------------------------------------------+ //| CLASS 9 — Place chart drawing objects from tool interactions | //+------------------------------------------------------------------+ class CDrawingEngine : public CChartEventHandler { protected: int m_drawnObjectCounter; // Running counter used to generate unique object names int m_toolDrawingClickCount;// Number of chart clicks recorded for the current tool placement datetime m_drawPoint1Time; // Chart time of the first placement click datetime m_drawPoint2Time; // Chart time of the second placement click double m_drawPoint1Price; // Chart price of the first placement click double m_drawPoint2Price; // Chart price of the second placement click protected: //--- Generate a unique name for the next drawing object string MakeUniqueObjectName(); //--- Process a chart click and dispatch to the appropriate object creator void HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction); //--- Create a chart object that requires a single placement click void CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType); //--- Create a chart object that requires two placement clicks void CreateTwoClickObject(int sub, TOOL_TYPE toolType); //--- Create a chart object that requires three placement clicks void CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType); }; //+------------------------------------------------------------------+ //| Generate a unique name for the next drawing object | //+------------------------------------------------------------------+ string CDrawingEngine::MakeUniqueObjectName() { //--- Increment the counter and combine it with the current time for uniqueness m_drawnObjectCounter++; return "ToolsPalette_Drawing_" + IntegerToString(m_drawnObjectCounter) + "_" + IntegerToString((int)TimeCurrent()); } //+------------------------------------------------------------------+ //| Process a chart click and dispatch to the appropriate creator | //+------------------------------------------------------------------+ void CDrawingEngine::HandleDrawingClick(int mouseX, int mouseY, TOOL_TYPE &activeTool, string &instruction) { //--- Convert screen coordinates to chart time and price datetime barTime; double barPrice; int sub; if (!ChartXYToTimePrice(m_chartId, mouseX, mouseY, sub, barTime, barPrice)) return; //--- Exit if the active tool requires no clicks int clicksNeeded = GetRequiredClickCount(activeTool); if (clicksNeeded <= 0) return; //--- Increment the click count for the ongoing placement sequence m_toolDrawingClickCount++; if (m_toolDrawingClickCount == 1) { //--- Record the first anchor point m_drawPoint1Time = barTime; m_drawPoint1Price = barPrice; //--- Create object immediately for single-click tools if (clicksNeeded == 1) { CreateSingleClickObject(sub, barTime, barPrice, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } else instruction = "Click second point for " + GetToolLabel(activeTool) + "."; } else if (m_toolDrawingClickCount == 2) { //--- Record the second anchor point m_drawPoint2Time = barTime; m_drawPoint2Price = barPrice; //--- Create object immediately for two-click tools if (clicksNeeded == 2) { CreateTwoClickObject(sub, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } else instruction = "Click third point for " + GetToolLabel(activeTool) + "."; } else if (m_toolDrawingClickCount == 3) { //--- Create object for three-click tools using all three recorded anchor points CreateThreeClickObject(sub, barTime, barPrice, activeTool); m_toolDrawingClickCount = 0; activeTool = TOOL_NONE; instruction = ""; } } //+------------------------------------------------------------------+ //| Create a chart object that requires a single placement click | //+------------------------------------------------------------------+ void CDrawingEngine::CreateSingleClickObject(int sub, datetime t, double p, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; switch (toolType) { //--- Create a horizontal line at the clicked price case TOOL_HLINE: ok = ObjectCreate(m_chartId, name, OBJ_HLINE, 0, 0, p); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); } break; //--- Create a vertical line at the clicked time case TOOL_VLINE: ok = ObjectCreate(m_chartId, name, OBJ_VLINE, 0, t, 0); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); ObjectSetInteger(m_chartId, name, OBJPROP_STYLE, STYLE_DASH); } break; //--- Create a text label at the clicked position case TOOL_TEXT: ok = ObjectCreate(m_chartId, name, OBJ_TEXT, sub, t, p); if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, "Text"); ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrWhite); ObjectSetInteger(m_chartId, name, OBJPROP_FONTSIZE, 10); } break; //--- Create an arrow-up annotation at the clicked position case TOOL_ARROW_UP: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_UP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Create an arrow-down annotation at the clicked position case TOOL_ARROW_DOWN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_DOWN, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Create a thumbs-up annotation at the clicked position case TOOL_THUMB_UP: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_UP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Create a thumbs-down annotation at the clicked position case TOOL_THUMB_DOWN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_THUMB_DOWN, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Create a left price label at the clicked position case TOOL_PRICE_LABEL: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_LEFT_PRICE, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrDodgerBlue); break; //--- Create a stop sign annotation at the clicked position case TOOL_STOP_SIGN: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_STOP, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrRed); break; //--- Create a check mark annotation at the clicked position case TOOL_CHECK_MARK: ok = ObjectCreate(m_chartId, name, OBJ_ARROW_CHECK, sub, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrLime); break; //--- Create a Fibonacci time zones object at the clicked position case TOOL_FIBO_TIMEZONES: ok = ObjectCreate(m_chartId, name, OBJ_FIBOTIMES, sub, t, p, t, p); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, clrGold); break; default: break; } //--- Mark created objects as selectable and trigger a chart redraw if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } } //+------------------------------------------------------------------+ //| Create a chart object that requires two placement clicks | //+------------------------------------------------------------------+ void CDrawingEngine::CreateTwoClickObject(int sub, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve the two recorded anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Create a standard trend line between the two anchor points case TOOL_TRENDLINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); break; //--- Create a ray line starting at the first anchor and extending right case TOOL_RAY: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true); break; //--- Create an extended line running through both anchors in both directions case TOOL_EXTENDED_LINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_RAY_LEFT, true); ObjectSetInteger(m_chartId, name, OBJPROP_RAY_RIGHT, true); } break; //--- Create a measure/info line and annotate it with pip distance case TOOL_INFO_LINE: ok = ObjectCreate(m_chartId, name, OBJ_TREND, sub, t1, p1, t2, p2); if (ok) { ObjectSetString(m_chartId, name, OBJPROP_TEXT, StringFormat("%.0f pips", MathAbs(p2 - p1) / SymbolInfoDouble(_Symbol, SYMBOL_POINT) / 10.0)); objColor = clrMediumSlateBlue; } break; //--- Create a filled rectangle between the two anchor points case TOOL_RECTANGLE: ok = ObjectCreate(m_chartId, name, OBJ_RECTANGLE, sub, t1, p1, t2, p2); if (ok) ObjectSetInteger(m_chartId, name, OBJPROP_FILL, true); break; //--- Create a triangle with a computed third vertex case TOOL_TRIANGLE: ok = ObjectCreate(m_chartId, name, OBJ_TRIANGLE, sub, t1, p1, t2, p2, t1 + (t2 - t1) / 2, p1 - MathAbs(p2 - p1)); if (ok) objColor = clrMediumSlateBlue; break; //--- Create an ellipse using anchor points and a mid-edge third point case TOOL_ELLIPSE: ok = ObjectCreate(m_chartId, name, OBJ_ELLIPSE, sub, t1, p1, t2, p2, t1, p1 + (p2 - p1) / 2); if (ok) objColor = clrMediumOrchid; break; //--- Create a Fibonacci retracement between the two anchor points case TOOL_FIBO_RETRACEMENT: ok = ObjectCreate(m_chartId, name, OBJ_FIBO, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Fibonacci expansion between the two anchor points case TOOL_FIBO_EXPANSION: ok = ObjectCreate(m_chartId, name, OBJ_EXPANSION, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Fibonacci fan between the two anchor points case TOOL_FIBO_FAN: ok = ObjectCreate(m_chartId, name, OBJ_FIBOFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create Fibonacci arcs between the two anchor points case TOOL_FIBO_ARCS: ok = ObjectCreate(m_chartId, name, OBJ_FIBOARC, sub, t1, p1, t2, p2); if (ok) objColor = clrGold; break; //--- Create a Gann line between the two anchor points case TOOL_GANN_LINE: ok = ObjectCreate(m_chartId, name, OBJ_GANNLINE, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a Gann fan from the first anchor point case TOOL_GANN_FAN: ok = ObjectCreate(m_chartId, name, OBJ_GANNFAN, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a Gann grid between the two anchor points case TOOL_GANN_GRID: ok = ObjectCreate(m_chartId, name, OBJ_GANNGRID, sub, t1, p1, t2, p2); if (ok) objColor = clrOrangeRed; break; //--- Create a regression channel between the two anchor points case TOOL_REGRESSION_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_REGRESSION, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; //--- Create a standard deviation channel between the two anchor points case TOOL_STDDEV_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_STDDEVCHANNEL, sub, t1, p1, t2, p2); if (ok) objColor = clrCornflowerBlue; break; default: break; } //--- Apply shared properties and trigger a chart redraw if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, objColor); ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH, 1); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } } //+------------------------------------------------------------------+ //| Create a chart object that requires three placement clicks | //+------------------------------------------------------------------+ void CDrawingEngine::CreateThreeClickObject(int sub, datetime t3, double p3, TOOL_TYPE toolType) { string name = MakeUniqueObjectName(); bool ok = false; color objColor = clrDodgerBlue; //--- Retrieve the first two recorded anchor points datetime t1 = m_drawPoint1Time, t2 = m_drawPoint2Time; double p1 = m_drawPoint1Price, p2 = m_drawPoint2Price; switch (toolType) { //--- Create a parallel channel using all three anchor points case TOOL_PARALLEL_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_CHANNEL, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrCornflowerBlue; break; //--- Create a Fibonacci channel using all three anchor points case TOOL_FIBO_CHANNEL: ok = ObjectCreate(m_chartId, name, OBJ_FIBOCHANNEL, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrGold; break; //--- Create a pitchfork variant using all three anchor points case TOOL_PITCHFORK: case TOOL_SCHIFF_PITCHFORK: case TOOL_MOD_SCHIFF: ok = ObjectCreate(m_chartId, name, OBJ_PITCHFORK, sub, t1, p1, t2, p2, t3, p3); if (ok) objColor = clrMediumSeaGreen; break; default: break; } //--- Apply shared properties and trigger a chart redraw if (ok) { ObjectSetInteger(m_chartId, name, OBJPROP_COLOR, objColor); ObjectSetInteger(m_chartId, name, OBJPROP_WIDTH, 1); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTABLE, true); ObjectSetInteger(m_chartId, name, OBJPROP_SELECTED, true); ChartRedraw(m_chartId); } }
Мы объявляем класс "CDrawingEngine", который наследует от "CChartEventHandler" и добавляет защищенные члены для счетчика имен объектов, текущего количества щелчков в последовательности размещения и двух пар переменных времени и цены, хранящих первую и вторую якорные точки. Мы объявляем пять защищенных методов: "MakeUniqueObjectName" для генерации уникальных имен объектов, "HandleDrawingClick" для обработки каждого щелчка по графику и три метода создания для инструментов с одним, двумя и тремя щелчками.
Метод "MakeUniqueObjectName" увеличивает счетчик и объединяет его с текущим временем с помощью IntegerToString для получения уникальной строки с префиксом, содержащим идентификатор рисунка. Метод "HandleDrawingClick" преобразует экранные координаты во время и цену графика с помощью ChartXYToTimePrice, запрашивает необходимое количество кликов для активного инструмента, а затем отслеживает последовательность размещения. При первом клике он записывает первую точку привязки и либо немедленно создает объект для инструментов, активируемых одним щелчком, либо обновляет текст инструкции, подсказывающий для второй точки. При втором клике записывает вторую точку привязки и либо создает объект для инструментов с двумя кликами, либо запрашивает информацию для третьего. При третьем клике создаются объекты с тремя кликами, используя все записанные точки привязки. После каждого успешного создания счетчик кликов и активный инструмент сбрасываются.
Метод "CreateSingleClickObject" обрабатывает двенадцать типов инструментов с помощью оператора switch. Горизонтальные и вертикальные линии создаются с помощью ObjectCreate с использованием OBJ_HLINE и OBJ_VLINE с пунктирным стилем. Текстовые метки, стрелочные аннотации, ползунки вверх и вниз, ценовые метки, знаки остановки, галочки и временные зоны Фибоначчи создают соответствующие типы объектов MQL5 с соответствующими цветами. Все созданные объекты помечаются как выбираемые и выбранные.
Метод "CreateTwoClickObject" обрабатывает шестнадцать типов инструментов, используя две сохраненные якорные точки. Линии тренда, лучи и продолженные линии используют OBJ_TREND, при этом свойства лучей переключаются соответствующим образом. Информационная линия добавляет аннотацию расстояния в пипсах. Прямоугольники создаются с включенной заливкой, треугольники автоматически вычисляют третью вершину, а эллипсы используют третью точку посередине ребра. Коррекции Фибоначчи, расширения, веера и дуги используют каждый свой выделенный тип объекта золотым цветом. Инструменты Ганна используют оранжево-красный цвет, а типы каналов — васильково-синий. Все объекты получают общий цвет, ширину, возможность выбора и выбранные свойства.
Метод "CreateThreeClickObject" обрабатывает параллельные каналы, каналы Фибоначчи и варианты вил, используя все три точки привязки, создавая соответствующий тип объекта MQL5 и применяя общие свойства перед перерисовкой графика. Наконец, мы все подключаем заново, так что теперь все управление осуществляется из одного центрального класса.
Перестройка оболочки боковой панели верхнего уровня
Класс верхнего уровня развивается из минимальной оболочки в полноценный командный центр программы, управляющий жизненным циклом активных инструментов, маршрутизацией событий и полной инициализацией всех десяти классов в иерархии.
//+------------------------------------------------------------------+ //| CLASS 10 — Top-level sidebar shell exposing the public interface | //+------------------------------------------------------------------+ class CToolsSidebar : public CDrawingEngine { private: TOOL_TYPE m_currentActiveTool; // Currently active drawing tool, or TOOL_NONE string m_currentInstruction; // Instruction text shown during multi-click tool placement public: CToolsSidebar() { InitDefaults(); } // Construct and apply default state ~CToolsSidebar() { Destroy(); } // Destruct and clean up all resources //--- Initialize the sidebar and build all canvas objects bool Init(long chartId); //--- Destroy all canvas objects and release resources void Destroy(); //--- Handle all incoming chart events for the sidebar void OnEvent(const int id, const long &lp, const double &dp, const string &sp); private: //--- Set all member variables to their compile-time default values void InitDefaults(); //--- Toggle the given tool on or off as the active drawing tool void ToggleTool(TOOL_TYPE toolType); //--- Deactivate the current tool and reset placement state void DeactivateCurrentTool(); //--- Remove all drawing objects placed by this indicator instance void CleanupAllDrawnObjects(); }; //+------------------------------------------------------------------+ //| Set all member variables to their compile-time default values | //+------------------------------------------------------------------+ void CToolsSidebar::InitDefaults() { //--- Reset chart reference to default m_chartId = 0; //--- Set the sidebar bitmap label object name m_nameSidebar = "ToolsPalette_Sidebar"; //--- Set the flyout bitmap label object name m_nameFlyout = "ToolsPalette_Flyout"; //--- Set supersampling factor for high-res rendering m_supersampleFactor = 4; //--- Set default button size and spacing m_categoryButtonSize = 36; m_categoryButtonPadding = 6; //--- Set panel corner rounding radius m_panelCornerRadius = 10; //--- Set header and grip strip height m_headerGripHeight = 92; //--- Set sidebar panel width m_sidebarWidth = 48; //--- Reset computed height and visible category count m_sidebarHeight = 0; m_sidebarMaxVisibleCats = 0; //--- Reset scroll state m_sidebarScrollPixels = 0; m_sidebarScrollThumbHeight = 30; m_sidebarScrollThinWidth = 3; m_isSidebarThumbDragging = false; m_sidebarThumbDragStartY = 0; m_sidebarThumbDragStartPixels = 0; m_isHoveredSidebarScrollArea = false; m_isHoveredSidebarThumb = false; //--- Reset panel position to origin m_panelX = 0; m_panelY = CanvasY; //--- Default snap state to left edge m_snapState = SNAP_LEFT; //--- Reset panel drag state m_isPanelDragging = false; m_dragOffsetX = 0; m_dragOffsetY = 0; //--- Reset snapped height override and resize state m_snappedSidebarHeight = 0; m_isResizingBottomEdge = false; m_bottomResizeDragStartY = 0; m_bottomResizeStartHeight = 0; m_isBottomResizeHovered = false; //--- Reset all header button hover flags m_hoveredCategory = CAT_NONE; m_isCloseButtonHovered = false; m_isThemeButtonHovered = false; m_isGripAreaHovered = false; //--- Set flyout layout defaults m_flyoutWidth = 195; m_flyoutItemHeight = 32; m_flyoutPadding = 7; m_flyoutPointerWidth = 10; m_flyoutPointerHeight = 8; m_flyoutPointerLocalY = 40; m_flyoutPointerOnLeft = true; //--- Reset flyout visibility and category state m_isFlyoutVisible = false; m_flyoutActiveCat = CAT_NONE; m_hoveredFlyoutItem = -1; //--- Reset flyout scroll state m_flyoutScrollPixels = 0; m_flyoutMaxVisibleItems = 5; m_flyoutScrollThumbHeight = 30; m_isFlyoutThumbDragging = false; m_flyoutThumbDragStartY = 0; m_flyoutThumbDragStartPixels = 0; m_isHoveredFlyoutScrollArea = false; m_isHoveredFlyoutThumb = false; //--- Apply the starting theme from user input m_isDarkTheme = StartDark; //--- Reset mouse button tracking m_previousMouseButtonState = 0; //--- Reset active tool and instruction text m_currentActiveTool = TOOL_NONE; m_currentInstruction = ""; //--- Reset drawing object placement state m_drawnObjectCounter = 0; m_toolDrawingClickCount = 0; m_drawPoint1Time = 0; m_drawPoint2Time = 0; m_drawPoint1Price = 0.0; m_drawPoint2Price = 0.0; } //+------------------------------------------------------------------+ //| Initialize the sidebar and build all canvas objects | //+------------------------------------------------------------------+ bool CToolsSidebar::Init(long chartId) { //--- Reset all members to defaults before initializing InitDefaults(); //--- Store the target chart identifier m_chartId = chartId; //--- Set initial panel X position based on default snap state m_panelX = (m_snapState == SNAP_RIGHT) ? (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS) - m_sidebarWidth : 0; //--- Populate all category and tool definitions InitAllCategoriesAndTools(); //--- Apply the active theme color set ApplyTheme(); //--- Compute and set the sidebar panel height CalcSidebarHeight(); //--- Create all canvas layers; abort on failure if (!CreateAllCanvases(m_sidebarWidth, m_sidebarHeight)) return false; //--- Position the sidebar chart object ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); ObjectSetInteger(0, m_nameSidebar, OBJPROP_ZORDER, 100); //--- Set the flyout Z-order above the sidebar ObjectSetInteger(0, m_nameFlyout, OBJPROP_ZORDER, 200); //--- Hide the flyout and draw the initial sidebar frame HideFlyout(); DrawSidebar(m_currentActiveTool); //--- Reapply position in case the draw call shifted the object ObjectSetInteger(0, m_nameSidebar, OBJPROP_XDISTANCE, m_panelX); ObjectSetInteger(0, m_nameSidebar, OBJPROP_YDISTANCE, m_panelY); //--- Enable mouse move and wheel events ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); ChartSetInteger(0, CHART_MOUSE_SCROLL, true); return true; } //+------------------------------------------------------------------+ //| Destroy all canvas objects and release resources | //+------------------------------------------------------------------+ void CToolsSidebar::Destroy() { //--- Deactivate the active tool before destroying m_currentActiveTool = TOOL_NONE; //--- Delegate canvas cleanup to the layer base class DestroyAllCanvases(); //--- Remove all chart drawing objects placed by this instance CleanupAllDrawnObjects(); //--- Restore chart mouse scroll to the default state ChartSetInteger(0, CHART_MOUSE_SCROLL, true); } //+------------------------------------------------------------------+ //| Toggle the given tool on or off as the active drawing tool | //+------------------------------------------------------------------+ void CToolsSidebar::ToggleTool(TOOL_TYPE toolType) { //--- Deactivate if the pointer tool is selected or the tool is already active if (toolType == TOOL_POINTER || m_currentActiveTool == toolType) { m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; } else { //--- Activate the new tool and reset its click counter and instruction m_currentActiveTool = toolType; m_toolDrawingClickCount = 0; m_currentInstruction = "Click on chart to place " + GetToolLabel(toolType) + "."; } } //+------------------------------------------------------------------+ //| Deactivate the current tool and reset placement state | //+------------------------------------------------------------------+ void CToolsSidebar::DeactivateCurrentTool() { //--- Clear tool state and instruction text m_currentActiveTool = TOOL_NONE; m_toolDrawingClickCount = 0; m_currentInstruction = ""; //--- Redraw the sidebar to reflect the deactivated state DrawSidebar(m_currentActiveTool); ChartRedraw(); } //+------------------------------------------------------------------+ //| Remove all drawing objects placed by this indicator instance | //+------------------------------------------------------------------+ void CToolsSidebar::CleanupAllDrawnObjects() { //--- Iterate backwards through all chart objects to safely delete matches int total = ObjectsTotal(m_chartId); for (int i = total - 1; i >= 0; i--) { string n = ObjectName(m_chartId, i); //--- Delete objects whose names match the drawing prefix if (StringFind(n, "ToolsPalette_Drawing_") == 0) ObjectDelete(m_chartId, n); } } //+------------------------------------------------------------------+ //| Handle all incoming chart events for the sidebar | //+------------------------------------------------------------------+ void CToolsSidebar::OnEvent(const int id, const long &lp, const double &dp, const string &sp) { //--- Deactivate the active tool when Escape is pressed if (id == CHARTEVENT_KEYDOWN && lp == 27) { DeactivateCurrentTool(); return; } if (id == CHARTEVENT_MOUSE_MOVE) { int mouseX = (int)lp, mouseY = (int)dp, mouseButtons = (int)sp; int lx, ly, flx, fly; bool overSidebar = HitTestOverSidebar(mouseX, mouseY, lx, ly); bool overFlyout = !overSidebar && HitTestOverFlyout(mouseX, mouseY, flx, fly); //--- Handle theme toggle on fresh left-click over the theme button if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_isThemeButtonHovered) { ToggleTheme(); DrawSidebar(m_currentActiveTool); if (m_isFlyoutVisible) DrawFlyoutForCategory(m_flyoutActiveCat, m_currentActiveTool); ChartRedraw(); m_previousMouseButtonState = mouseButtons; return; } //--- Handle tool selection from flyout on fresh left-click over a flyout item if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE) { int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (m_hoveredFlyoutItem < nT) { //--- Toggle the selected tool and redraw ToggleTool(m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].toolType); HideFlyout(); DrawSidebar(m_currentActiveTool); ChartRedraw(); } m_previousMouseButtonState = mouseButtons; return; } //--- Handle direct tool selection for single-tool categories on fresh left-click if (mouseButtons == 1 && m_previousMouseButtonState == 0 && overSidebar && m_hoveredCategory != CAT_NONE && !m_isCloseButtonHovered && !m_isThemeButtonHovered && !m_isGripAreaHovered && ArraySize(m_categories[(int)m_hoveredCategory].tools) == 1) { ToggleTool(m_categories[(int)m_hoveredCategory].tools[0].toolType); HideFlyout(); DrawSidebar(m_currentActiveTool); ChartRedraw(); m_previousMouseButtonState = mouseButtons; return; } //--- Handle chart placement clicks when a drawing tool is active if (mouseButtons == 1 && m_previousMouseButtonState == 0 && m_currentActiveTool != TOOL_NONE && m_currentActiveTool != TOOL_POINTER && !overSidebar && !overFlyout) { HandleDrawingClick(mouseX, mouseY, m_currentActiveTool, m_currentInstruction); DrawSidebar(m_currentActiveTool); m_previousMouseButtonState = mouseButtons; return; } //--- Forward remaining mouse move logic to the chart event handler RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); //--- Update the sidebar tooltip based on current hover state string tip = ""; if (overSidebar && m_hoveredCategory != CAT_NONE) tip = m_categories[(int)m_hoveredCategory].categoryLabel; if (overFlyout && m_hoveredFlyoutItem >= 0 && m_flyoutActiveCat != CAT_NONE) { int nT = ArraySize(m_categories[(int)m_flyoutActiveCat].tools); if (m_hoveredFlyoutItem < nT) tip = m_categories[(int)m_flyoutActiveCat].tools[m_hoveredFlyoutItem].tooltipText; } ObjectSetString(0, m_nameSidebar, OBJPROP_TOOLTIP, tip); return; } //--- Forward all other chart events to the routing handler RouteChartEvent(id, lp, dp, sp, m_currentActiveTool); }
Во-первых, мы объявляем класс "CToolsSidebar", который теперь наследует от "CDrawingEngine" вместо "CSidebarRenderer", как в предыдущей части, что дает ему доступ ко всей иерархии из десяти классов. Мы добавляем два закрытых члена: текущий активный тип инструмента и строку инструкций, отображаемую при размещении несколькими щелчками мыши. Открытый интерфейс остается прежним с "Init", "Destroy" и «OnEvent", в то время как четыре приватных метода обрабатывают значения по умолчанию, переключение инструментов, деактивацию инструментов и очистку объектов рисования.
Далее метод "InitDefaults" сбрасывает все переменные-члены во всей иерархии до безопасных значений по умолчанию. Помимо геометрии боковой панели, имен холстов и настроек темы из предыдущей части, теперь мы инициализируем имя bitmap label-объекта всплывающего окна, все переменные состояния прокрутки как для боковой панели, так и для выдвижной панели, состояние перетаскивания панели и изменения размера нижней части, флаги наведения для всех кнопок заголовка, размеры раскладки выдвижной панели и конфигурацию указателя, видимость выдвижной панели и состояние прокрутки, отслеживание кнопок мыши, текст активных инструментов и инструкций, а также счетчик графического движка и хранилище якорных точек.
Метод "Init" управляет всей последовательностью запуска. Мы вызываем "InitDefaults", сохраняем идентификатор диаграммы, вычисляем начальное положение панели на основе состояния привязки, вызываем "InitAllCategoriesAndTools" для заполнения полного реестра инструментов, применяем тему, вычисляем высоту боковой панели, создаем все четыре холста, позиционируем объекты графика боковой панели и задаем для выдвижной панели более высокий Z-порядок, скрываем выдвижную панель, рисуем начальную рамку боковой панели и включаем события перемещения мыши и колеса мыши с помощью функции ChartSetInteger.
Метод Destroy деактивирует текущий инструмент, делегирует очистку холста методу "DestroyAllCanvases", вызывает метод "CleanupAllDrawnObjects" для удаления всех размещенных объектов графика путем обратного перебора ObjectsTotal и удаления имен, соответствующих префиксу рисования, с помощью StringFind, и восстанавливает прокрутку графика.
Метод "ToggleTool" деактивирует инструмент, если указатель выбран или тот же инструмент нажат снова, очищая счетчик кликов и инструкцию. В противном случае он активирует новый инструмент, сбрасывает счетчик и устанавливает текст инструкции с помощью метода "GetToolLabel". Метод "DeactivateCurrentTool" очищает состояние инструмента, перерисовывает боковую панель и вызывается при нажатии клавиши Escape.
Метод "OnEvent" является основной точкой входа для всех событий графика. Он обрабатывает нажатия клавиши Escape для деактивации инструмента, а затем обрабатывает события перемещения мыши в цепочке приоритетов: щелчки по переключателю темы вызывают функцию "ToggleTheme" и перерисовывают обе панели, щелчки по элементам выдвижной панели вызывают функцию "ToggleTool" с выбранным инструментом и скрывают выдвижную панель, щелчки по категориям отдельных инструментов активируют инструмент напрямую, а щелчки по размещению графика, когда активен инструмент рисования, вызывают функцию "HandleDrawingClick". Вся оставшаяся логика работы с мышью передается в функцию "RouteChartEvent". Мы также обновляем всплывающую подсказку боковой панели в зависимости от состояния при наведении курсора, используя функцию ObjectSetString. Все остальные типы событий перенаправляются непосредственно обработчику маршрутизации. Таким образом, теперь вся иерархия управления соединена между собой, что и позволяет достичь нашей цели. Далее мы протестируем программу в следующем разделе.
Тестирование на истории
Мы скомпилировали программу и прикрепили ее к графику. Ниже показана итоговая визуализация в виде GIF-изображения.

Во время тестирования выдвижные панели отображались корректно при наведении курсора на каждую кнопку категории с плавным выравниванием треугольника указателя, выбор инструмента из выдвижной панели точно размещал объекты графика при использовании инструментов с одним, двумя и тремя щелчками, а перетаскивание панели корректно привязывалось к обоим краям графика, после чего происходило изменение положения выдвижной панели.
Заключение
В заключение, мы не просто создали более красивую боковую панель, но и работающий, тестируемый стек интерактивных элементов на языке MQL5. В частности, статическая палитра была расширена до системы из десяти классов, которая предоставляет управляемый данными реестр инструментов, охватывающий тридцать пять инструментов в восьми категориях, поэтому новые инструменты добавляются в виде записей данных, а не путем внесения изменений в код. Выдвижная панель выбора включает в себя треугольник указателя, обрезанную прокрутку, состояния при наведении курсора и подсветке, а также тонкую полоску прокрутки с ползунком. Обработчик событий графика маршрутизирует события перемещения мыши, колесика мыши, клавиатуры и изменения размера графика, выполняя при этом точную проверку попадания в области боковой панели и выдвижной панели. Полная интерактивность панели включает перетаскивание с привязкой к краям, изменение размера по нижнему краю, прокручиваемые списки категорий и переключение тем в реальном времени без перезапуска. Наконец, механизм рисования преобразует выбор инструмента в объекты графика с детерминированным сбросом состояния после каждого завершенного размещения.
Критерии приемки, продемонстрированные в тестировании, ясны: выбор инструмента из выдвижной панели приводит к размещению объекта на графике; переполнение списков происходит при прокрутке колесом мыши или перетаскивании ползунком. Панель перетаскивается и привязывается к краям графика. Переключатели тем применяются немедленно, а выдвижные панели корректно перемещаются при перемещении панели. Архитектура обеспечивает четкий контракт взаимодействия — активный инструмент, необходимые щелчки и состояние размещения — что делает поведение предсказуемым и упрощает его расширение. После прочтения статьи вы сможете:
- Выбирать инструменты рисования из выдвижных панелей и размещать линии тренда, коррекции Фибоначчи, каналы, вилы и аннотации непосредственно на боковой панели
- Перетаскивать, изменять размер и привязывать боковую панель к краям графика во время реальных сессий, не перекрывая ценовое движение на графике
- Расширять реестр инструментов, добавляя новые инструменты в качестве записей данных без изменения логики отображения или взаимодействия
В следующей части мы будем использовать этот слой взаимодействия для улучшения инструмента перекрестия с помощью наложения рисок прицела, линий перекрестия во всю ширину и высоту с метками осей, круговой лупы, отображающей увеличенное содержимое свечей, и режима измерения двойным щелчком с диагональной линией, якорными маркерами и плавающей статистикой баров и пунктов. Оставайтесь с нами.
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/22214
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Разработка инструментария для анализа Price Action (Часть 68): Панель RSI с привязкой к цене на языке MQL5
Рыночные секреты Ларри Уильямса (Часть 14): Обнаружение разворотов Hidden Smash Day с помощью пользовательского индикатора
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Выглядит ЗДОРОВО; спасибо, что поделились кодами и идеями; берегите себя
Какой прекрасный вид. Спасибо.