Торговые инструменты на MQL5 (Часть 14): Прокручиваемый текстовый холст с пиксельной точностью, сглаживанием и закругленной полосой прокрутки
Введение
В своей предыдущей статье (Часть 13) мы разработали ценовую панель на основе холста в MetaQuotes Language 5 (MQL5) с использованием класса CCanvas для интерактивных панелей, визуализирующих графики цен с линейными графиками и эффектами тумана, а также статистику по показателям счета и детали баров с поддержкой фоновых изображений, градиенты, переключение тем оформления, перетаскивание, и изменение размера с помощью обработки событий. В Части 14 мы улучшим панель с помощью прокручиваемого текстового холста с пиксельной точностью сглаживанием и закругленной полосой прокрутки, чтобы обойти ограничения MQL5. Это усовершенствование включает текстовую панель для руководств по использованию с плавной прокруткой с помощью пользовательских элементов со сглаживанием. Оно включает в себя полосу прокрутки, расширяемую при наведении курсора мыши, с кнопками/ползунком, поддержкой функции колесика, тематическими фонами/непрозрачностью и динамическим переносом линий/раскрашиванием, которые плавно интегрированы для предоставления подробных инструкций пользователя. В статье рассмотрим следующие темы:
- Изучение структуры прокручиваемого текстового холста с пиксельной точностью
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
В итоге у нас будет панель холста в MQL5, теперь включающая в себя подробную и интерактивную текстовую панель руководства, готовую к дальнейшей настройке. Давайте погрузимся в процесс!
Изучение структуры прокручиваемого текстового холста с пиксельной точностью
Эта идеально точная прокручиваемая текстовая структура решает проблемы, связанные с ограничениями MQL5 встроенной функции прокрутки текста, за счет использования пользовательского рендеринга на уровне пикселей со сглаживанием для получения плавных краев, закругленной полосы прокрутки, которая расширяется при наведении курсора для повышения удобства использования, а также интерактивных элементов, таких как кнопки вверх/вниз и перетаскиваемый ползунок для навигации по длинному контенту, включая руководства по использованию. Она поддерживает фоны тем оформления с регулируемой непрозрачностью, динамический перенос линий в соответствии с шириной панели с сохранением цветов заголовков / ссылок и прокрутку колесика мыши в теле текста, чтобы избежать помех при масштабировании графика, обеспечивая точное управление без использования встроенных объектов. Интеграция с дашбордом позволяет оперативно обновлять информацию о событиях, поддерживая согласованность графиков/статистики/ и текстовых панелей для создания целостного инструмента мониторинга.
Мы решили не использовать статические объекты в MQL5 для линий и хотим полностью изучить возможности холста. Хорошо то, что с холстом нам на самом деле не нужно беспокоиться о перетекании текста через рамки, как это было в предыдущих статьях, в которых мы использовали встроенные объекты; холст автоматически обрезает тексты, помогая нам добиться эффекта прокрутки веб-сайта. Кроме того, закругленная динамическая полоса прокрутки была подсказанапривлекательным оформлением терминалов MetaQuotes, которое появилось в последних обновлениях. Взгляните на то, чего мы в итоге хотим достичь в результате такого влияния.

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

Реализация средствами MQL5
Для улучшения программы в MQL5 нам потребуется объявить новую текстовую панель canvas (холст) и добавить некоторые дополнительные входные параметры и глобальные переменные для динамического управления ее отображением.
//+------------------------------------------------------------------+ //| Canvas Dashboard PART2.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 //+------------------------------------------------------------------+ //| Canvas objects | //+------------------------------------------------------------------+ //--- CCanvas canvasText; // Declare text canvas //+------------------------------------------------------------------+ //| Canvas names | //+------------------------------------------------------------------+ //--- string canvasTextName = "TextCanvas"; // Set text canvas name //--- Added Extra inputs input bool EnableTextPanel = true; // Enable Text Panel input int TextPanelHeight = 200; // Text Panel Height input double TextBackgroundOpacityPercent = 85.0; // Text Background Opacity Percent input double StatsHeaderBgOpacityPercent = 20.0; // Stats Header Bg Opacity Percent input int StatsHeaderBgRadius = 8; // Stats Header Bg Radius //--- Extended the globals uint bg_pixels_text[]; //--- Store text background pixels int prev_mouse_state = 0; //--- Initialize previous mouse state int last_mouse_x = 0, last_mouse_y = 0; //--- Initialize last mouse positions int header_height = 27; //--- Set header height int gap_y = 7; //--- Set Y gap int button_size = 25; //--- Set button size int theme_x_offset = -75; //--- Set theme X offset int minimize_x_offset = -50; //--- Set minimize X offset int close_x_offset = -25; //--- Set close X offset bool panels_minimized = false; //--- Initialize panels minimized flag color HeaderColor = C'60,60,60'; //--- Set header color color HeaderHoverColor = clrRed; //--- Set header hover color color HeaderDragColor = clrMediumBlue; //--- Set header drag color
Первое, что мы делаем для реализации улучшений, - это объявляем дополнительный объект CCanvas с именем "canvasText" для новой текстовой панели, которая будет обрабатывать прокручиваемое содержимое руководства по использованию. Мы добавляем строковую константу "canvasTextName" со значением "TextCanvas", чтобы однозначно идентифицировать этот холст, как и другие. Далее мы расширяем входные параметры для поддержки текстовой панели: Включим "EnableTextPanel" в качестве логического значения по умолчанию, равного true, для её включения / выключения, "TextPanelHeight" - двести пикселей, чтобы задать её высоту, "TextBackgroundOpacityPercent" - 85,0 для контроля прозрачности фона, "StatsHeaderBgOpacityPercent" - 20,0 для непрозрачности фона заголовка статистики и "StatsHeaderBgRadius" - в значении восемь для закругления углов заголовков статистики. Добавляем новый массив uint "bg_pixels_text" для хранения масштабированных пикселей фона для текстовой панели, обеспечивая согласованность с графиком и статистикой, если используются фоновые изображения.
Далее мы сохраняем и инициализируем глобальные параметры для взаимодействия с мышью. Сюда относится значение "prev_mouse_state" равное нулю, а также значения "last_mouse_x" и "last_mouse_y" равные нулю для отслеживания позиций. Устанавливаем "header_height" в значение двадцать семь, а "gap_y" - в значение семь для показателя интервала по вертикали. Значение параметра "button_size" равно двадцати пяти. Для размещения кнопок относительно правого края заголовка мы используем смещения: "theme_x_offset" равное минус семьдесят пять, "minimize_x_offset" равное минус пятьдесят и "close_x_offset" равное минус двадцать пять. Мы также сохраняем значение "panels_minimized" как false, чтобы начать расширение. Цвета определены: Для состояний заголовка используются следующие значения: "HeaderColor" — средне-серый (medium gray), "HeaderHoverColor" — красный и "HeaderDragColor" — сине-голубой (medium blue). Следующий шаг — добавить текст для отображения на текстовую панель и определить его глобальные переменные управления следующим образом.
string text_usage_text = "\nCanvas Dashboard Usage Guide\n\n" "Welcome to the Canvas Dashboard – Your Interactive Tool for Real-Time Market Monitoring in MetaTrader 5!\n\n" "Enhance your trading experience with this dynamic dashboard that visualizes price data, account stats, and interactive controls. Designed for ease of use, it allows customization through dragging, resizing, theme switching, and more, while providing essential market insights at a glance.\n\n" "Key Features:\n" "- Price Graph Panel: Displays recent bar closes with a fog gradient fill, background image support, and resize indicators.\n" "- Stats Panel: Shows account balance, equity, and current bar OHLC values with customizable backgrounds (single color or gradient).\n" "- Header Controls: Drag to move the dashboard; buttons for theme toggle (dark/light), minimize/maximize panels, and close.\n" "- Text Panel: Scrollable guide (this panel) with hover-expand scrollbar, up/down buttons, and slider for navigation.\n" "- Interactivity: Hover for highlights/tooltips; resize via borders (bottom, right, corner); wheel scroll in text area.\n" "- Theme Support: Switch between dark and light modes for better visibility on different chart backgrounds.\n" "- Background Options: Enable images with fog overlay; blend modes for transparency.\n\n" "Usage Instructions:\n" "1. Move the Dashboard: Click and drag the header (excluding buttons) to reposition on the chart.\n" "2. Resize Panels: Hover near the graph's bottom/right edges; click and drag when the icon appears (arrows for direction).\n" "3. Toggle Theme: Click the '[' icon in the header to switch between dark and light modes.\n" "4. Minimize/Maximize: Click the 'r' or 'o' icon to hide/show panels (header remains visible).\n" "5. Navigate Text: Use the scrollbar (hovers to expand with buttons/slider); click up/down or drag slider; wheel scroll in body.\n" "6. Close Dashboard: Click the 'X' icon in the header to remove it from the chart.\n\n" "Important Notes:\n" "- Risk Disclaimer: This dashboard is for informational purposes only. Always verify data and trade responsibly.\n" "- Compatibility Check: Ensure chart settings allow mouse events; test on demo for interactions.\n" "- Optimization Tips: Adjust input parameters like graph bars, fonts, colors, and opacity for your setup.\n" "- Security Measures: No account modifications; use on trusted platforms.\n" "- Legal Notice: No guarantees of accuracy. Consult professionals as needed.\n\n" "Contact Methods:\n" "NB:\n" "********************************************\n" " >*** FOR SUPPORT, QUERIES, OR CUSTOMIZATIONS, REACH OUT IMMEDIATELY: ***<\n" " __________________________________________\n\n" "1. Email: mutiiriallan.forex@gmail.com (Primary Support Channel)\n" "2. Telegram Channel: @ForexAlgo-Trader (Updates & Community)\n" "3. Telegram Group: https://t.me/Forex_Algo_Trader (Direct Assistance & Discussions)\n\n" "********************************************\n\n" "Thank you for choosing our Canvas Dashboard solutions. Use wisely, monitor confidently, and elevate your trading journey! 🚀\n"; //--- Set text usage content int text_scroll_pos = 0; //--- Initialize text scroll position int text_max_scroll = 0; //--- Initialize text max scroll int text_slider_height = 20; //--- Set text slider height bool text_movingStateSlider = false; //--- Initialize text slider moving flag int text_mlbDownY_Slider = 0; //--- Initialize text slider down Y int text_mlbDown_YD_Slider = 0; //--- Initialize text slider down YD int text_total_height = 0; //--- Initialize text total height int text_visible_height = 0; //--- Initialize text visible height bool text_scroll_visible = false; //--- Initialize text scroll visible flag bool text_mouse_in_body = false; //--- Initialize text mouse in body flag bool prev_text_mouse_in_body = false; //--- Initialize previous text mouse in body flag bool text_scroll_up_hovered = false; //--- Initialize text scroll up hovered flag bool text_scroll_down_hovered = false; //--- Initialize text scroll down hovered flag bool text_scroll_slider_hovered = false; //--- Initialize text scroll slider hovered flag bool text_scroll_area_hovered = false; //--- Initialize text scroll area hovered flag const int TEXT_MAX_LINES = 100; //--- Set text max lines int text_adjustedLineHeight = 0; //--- Initialize text adjusted line height int text_scrollbar_full_width = 16; //--- Set text scrollbar full width int text_scrollbar_thin_width = 2; //--- Set text scrollbar thin width int text_track_width = 16; //--- Set text track width int text_scrollbar_margin = 5; //--- Set text scrollbar margin int text_button_size = 16; //--- Set text button size color text_leader_color_dark = C'45,45,45'; //--- Set dark text leader color color text_leader_color_light = C'200,200,200'; //--- Set light text leader color color text_button_bg_dark = C'60,60,60'; //--- Set dark text button bg color text_button_bg_light = C'220,220,220'; //--- Set light text button bg color text_button_bg_hover_dark = C'70,70,70'; //--- Set dark text button bg hover color text_button_bg_hover_light = C'180,180,180'; //--- Set light text button bg hover color text_arrow_color_dark = C'150,150,150'; //--- Set dark text arrow color color text_arrow_color_light = C'50,50,50'; //--- Set light text arrow color color text_arrow_color_disabled_dark = C'80,80,80'; //--- Set dark disabled arrow color color text_arrow_color_disabled_light = C'150,150,150'; //--- Set light disabled arrow color color text_arrow_color_hover_dark = C'100,100,100'; //--- Set dark hover arrow color color text_arrow_color_hover_light = C'0,0,0'; //--- Set light hover arrow color color text_slider_bg_dark = C'80,80,80'; //--- Set dark slider bg color text_slider_bg_light = C'150,150,150'; //--- Set light slider bg color text_slider_bg_hover_dark = C'100,100,100'; //--- Set dark slider bg hover color text_slider_bg_hover_light = C'100,100,100'; //--- Set light slider bg hover color text_bg_light = clrWhite; //--- Set light text bg color text_bg_dark = clrBlack; //--- Set dark text bg color text_base_light = clrBlack; //--- Set light text base color text_base_dark = clrWhite; //--- Set dark text base
Здесь мы определяем строку "text_usage_text", содержащую полное содержимое руководства по использованию, которое будет отображаться на текстовой панели, структурированное новыми строками для абзацев, разделами, такими как приветственное сообщение, ключевыми функциями с пунктами маркированного списка для панелей / элементов управления / интерактивности/тем/фона, инструкциями по использованию в виде пронумерованных шагов для перемещения/изменения размера / переключения / навигации / закрытия, важные замечания о рисках / совместимости / оптимизации / безопасности / юридических аспектах, способы связи с помощью электронной почты / Telegram и благодарственное письмо - все это отформатировано таким образом, чтобы обеспечить полное руководство пользователя. Это всего лишь произвольное форматирование текста, которое мы решили использовать в качестве демонстрации. Не стесняйтесь изменять его в соответствии с вашими конкретными потребностями.
Затем мы инициализируем переменные для управления прокруткой текста: "text_scroll_pos" - в нулевое значение для текущего смещения, "text_max_scroll" - в нулевое значение для максимального размера прокрутки, "text_slider_height" - в значение двадцать для размера ползунка, "text_movingStateSlider" - в значение false для отслеживания при перетаскивании ползунка, "text_mlbDownY_Slider" и "text_mlbDown_YD_Slider" - в нулевое значение для перемещения мыши в позиции на оси y во время взаимодействия с ползунком, значения "text_total_height" и "text_visible_height" в нулевое значение для полного содержимого и доступной для просмотра высоты, значение "text_scroll_visible" в значение false для управления отображением полосы прокрутки, "text_mouse_in_body" и "prev_text_mouse_in_body" в значение false для текущего/предыдущего состояния мыши в текстовом поле, флаги наведения курсора, такие как "text_scroll_up_hovered", в значение false для направления вверх / вниз / ползунка /области, значение константы "TEXT_MAX_LINES" равным сто в качестве ограничения буфера, а "text_adjustedLineHeight" в нулевое значение для межстрочного интервала. Устанавливаем размеры для полосы прокрутки: "text_scrollbar_full_width" - в зачение шестнадцать для развернутого состояния, "text_scrollbar_thin_width" - в зачение два для свернутого состояния, "text_track_width" - в зачение шестнадцать для установки ширины текста, "text_scrollbar_margin" - в зачение пять для отступов, "text_button_size" - в зачение шестнадцать для кнопок вверх/вниз. Определяем цвета тем оформления: выноска / кнопка bg / нормальная / при наведении курсора, стрелка нормальная / отключена / при наведении курсора для темного / светлого режимов, используя обозначения C'...', а базовый bg / текст - как белый / черный для светлой темы или черный / белый для темной, чтобы обеспечить видимость в разных темах оформления.
Далее обновим холст статистики, добавив закругленные прямоугольники. Сначала определим функцию для достижения этой цели.
//+------------------------------------------------------------------+ //| Fill rounded rectangle | //+------------------------------------------------------------------+ void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color) { if (radius <= 0) { cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color); //--- Fill rectangle if no radius return; //--- Exit } radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Adjust radius cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc cvs.FillCircle(x + radius, y + radius, radius, argb_color); //--- Fill top-left circle cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color); //--- Fill top-right circle cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color); //--- Fill bottom-right circle cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color); //--- Fill bottom-left circle cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal areas cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical areas }
Мы реализуем функцию "FillRoundedRectangle", чтобы нарисовать прямоугольник со скругленными углами на заданном контрольном объекте CCanvas, принимая параметры для положения x/y, ширины w/высоты h, радиуса угла и цвета ARGB. Если радиус равен нулю или меньше, мы просто заполняем стандартный прямоугольник от (x,y) до (x+w-1, y+h-1) методом FillRectangle и досрочно завершаем работу. В противном случае, чтобы избежать переполнения поля, мы ограничиваем радиус минимумом, равным половине ширины или высоты, используя MathMin функцию. Мы рисуем четверти дуги с помощью Arc для каждого угла: верхний левый угол от 180 до 270 градусов, верхний правый угол от 270 до 360, нижний правый угол от 0 до 90, нижний левый угол от 90 до 180. Все значения преобразуются в радианы с помощью вспомогательной функции, такой как "DegreesToRadians". Мы определим эту функцию позднее.
Затем заполняем полные круги в центре каждого угла с помощью FillCircle, чтобы придать форму закругленным областям. Затем заполняем горизонтальную среднюю полосу от левого радиуса до правого минус радиус по всей высоте и вертикальные боковые стороны от верхнего радиуса до нижнего минус радиус по всей ширине с помощью "FillRectangle", чтобы завершить форму без пробелов. Вспомогательная функция преобразования представлена ниже.
//+------------------------------------------------------------------+ //| Convert degrees to radians | //+------------------------------------------------------------------+ double DegreesToRadians(double degrees) { return((M_PI * degrees) / 180.0); //--- Perform conversion }
Мы просто делим умножение 'pi' или M_PI на целевые градусы на 180. Затем мы можем использовать функцию скругления прямоугольников (rounded rectangle) для отображения наших заголовков на панели статистики, обновив эту функцию.
//+------------------------------------------------------------------+ //| Update stats on canvas | //+------------------------------------------------------------------+ void UpdateStatsOnCanvas() { //--- string headerText = "Account Stats"; //--- Set header text canvasStats.FontSet("Arial Bold", StatsHeaderFontSize); //--- Set font int textW = canvasStats.TextWidth(headerText); //--- Get text width int textH = canvasStats.TextHeight(headerText); //--- Get text height int pad = 5; //--- Set padding int rectX = (statsWidth - textW) / 2 - pad; //--- Calculate rect X int rectY = yPos - pad / 2; //--- Calculate rect Y int rectW = textW + 2 * pad; //--- Calculate rect width int rectH = textH + pad; //--- Calculate rect height uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Calculate alpha uint argbHeaderBg = ColorToARGB(GetStatsHeaderBgColor(), alpha); //--- Convert bg to ARGB color header_border = is_dark_theme ? clrWhite : clrBlack; //--- Get border color uint argbHeaderBorder = ColorToARGB(header_border, 255); //--- Convert to ARGB FillRoundedRectangle(canvasStats, rectX - 1, rectY - 1, rectW + 2, rectH + 2, StatsHeaderBgRadius + 1, argbHeaderBorder); //--- Fill outer rectangle FillRoundedRectangle(canvasStats, rectX, rectY, rectW, rectH, StatsHeaderBgRadius, argbHeaderBg); //--- Fill inner rectangle uint argbHeader = ColorToARGB(headerColor, 255); //--- Convert header to ARGB canvasStats.TextOut(statsWidth / 2, yPos, headerText, argbHeader, TA_CENTER); //--- Draw header //--- }
Для реализации изменений модифицируем функцию "UpdateStatsOnCanvas", чтобы улучшить визуализацию заголовка с закругленным фоном для более «причесанного» вида. Для каждого заголовка, такого как 'Account Stats' или 'Current Bar Stats', мы устанавливаем шрифт Arial Bold в "StatsHeaderFontSize" с помощью FontSet, измеряем ширину и высоту текста, используя TextWidth и TextHeight, определяем отступы в пять пикселей и вычисляем положение / размеры прямоугольника, отцентрованного с помощью отступов: x как (ширина минус ширина текста)/2 минус отступ, y как текущее значение "yPos" минус половина отступа, ширина как текст плюс двойной отступ, высота как текст плюс отступ.
Мы вычисляем альфа-канал из "StatsHeaderBgOpacityPercent" более чем 100,0 раз по 255, преобразуем его в uchar, а цвет фона преобразуем из "GetStatsHeaderBgColor" в ARGB с этим альфа-каналом. Для рамок мы выбираем белый цвет в темной теме или черный в светлой, преобразуем в полную непрозрачность ARGB и вызываем функцию "FillRoundedRectangle" дважды: один раз для внешней рамки с позицией минус один, размером плюс два, радиусом плюс один, используя ARGB для рамки; затем для внутренней рамки с базовой позицией/размером/радиусом и фоном ARGB. Мы преобразуем цвет заголовка в ARGB с параметром 255, рисуем текст по центру с шириной статистики/2 и координатами "yPos" с помощью TextOut и выравнивания по центру, затем увеличиваем "yPos" на тридцать для создания интервала перед следующим разделом. Остальная часть функции остаётся такой, какой мы её определили ранее. После компиляции получаем следующий результат.

Мы видим, что заголовки панели статистики теперь заключены в закругленные прямоугольники. Следующее, что нам надо сделать, - это визуализировать текстовую панель. Мы также создадим для этого функцию, как и для других панелей, но сначала нам нужно будет обернуть текст, чтобы он был динамическим, а также добавить другие вспомогательные функции, которые нам понадобятся позже. Для достижения этого результата мы использовали следующий подход.
//+------------------------------------------------------------------+ //| Lighten color | //+------------------------------------------------------------------+ color LightenColor(color colorValue, double factor) { int blue = (int)MathMin(255, (colorValue & 0xFF) * factor); //--- Lighten blue int green = (int)MathMin(255, ((colorValue >> 8) & 0xFF) * factor); //--- Lighten green int red = (int)MathMin(255, ((colorValue >> 16) & 0xFF) * factor); //--- Lighten red return (color)(blue | (green << 8) | (red << 16)); //--- Return lightened color } //+------------------------------------------------------------------+ //| Calculate slider height | //+------------------------------------------------------------------+ int TextCalculateSliderHeight() { int scroll_area_height = TextPanelHeight - 2 - 2 * text_button_size; //--- Calculate area height int slider_min_height = 20; //--- Set min height if (text_total_height <= text_visible_height) return scroll_area_height; //--- Return full if no scroll double visible_ratio = (double)text_visible_height / text_total_height; //--- Calculate ratio int height = (int)MathFloor(scroll_area_height * visible_ratio); //--- Calculate height return MathMax(slider_min_height, height); //--- Return max of min and calculated } //+------------------------------------------------------------------+ //| Scroll text up | //+------------------------------------------------------------------+ void TextScrollUp() { if (text_adjustedLineHeight > 0 && text_scroll_pos > 0) { text_scroll_pos = MathMax(0, text_scroll_pos - text_adjustedLineHeight); //--- Scroll up UpdateTextOnCanvas(); //--- Update canvas } } //+------------------------------------------------------------------+ //| Scroll text down | //+------------------------------------------------------------------+ void TextScrollDown() { if (text_adjustedLineHeight > 0 && text_scroll_pos < text_max_scroll) { text_scroll_pos = MathMin(text_max_scroll, text_scroll_pos + text_adjustedLineHeight); //--- Scroll down UpdateTextOnCanvas(); //--- Update canvas } } //+------------------------------------------------------------------+ //| Update text hover effects | //+------------------------------------------------------------------+ void TextUpdateHoverEffects(int local_x, int local_y) { if (!text_scroll_visible) return; //--- Exit if no scroll int scrollbar_x = canvasText.Width() - text_track_width - 1; //--- Get scrollbar X int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height text_scroll_up_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1); //--- Check up hover int down_y = scrollbar_y + scrollbar_height - text_button_size; //--- Calculate down Y text_scroll_down_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= down_y && local_y <= down_y + text_button_size - 1); //--- Check down hover int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y text_scroll_slider_hovered = text_scroll_area_hovered && (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1 && local_y >= slider_y && local_y <= slider_y + text_slider_height - 1); //--- Check slider hover } //+------------------------------------------------------------------+ //| Get line color | //+------------------------------------------------------------------+ color GetLineColor(string lineText) { if (StringLen(lineText) == 0 || lineText == " ") return C'25,25,25'; //--- Return gray for empty if (StringFind(lineText, "mutiiriallan.forex@gmail.com") >= 0) return C'255,100,100'; //--- Return red for email if (StringFind(lineText, "https://t.me/Forex_Algo_Trader") >= 0) return C'150,100,200'; //--- Return purple for link if (StringFind(lineText, "@ForexAlgo-Trader") >= 0) return C'100,150,255'; //--- Return blue for channel if (StringFind(lineText, "http") >= 0 || StringFind(lineText, "t.me") >= 0) return C'100,150,255'; //--- Return blue for links string start3 = StringSubstr(lineText, 0, 3); //--- Get start substring if ((start3 == "1. " || start3 == "2. " || start3 == "3. " || start3 == "4. " || start3 == "5. ") && StringFind(lineText, "Initial Setup Instructions") < 0) return C'255,200,100'; //--- Return orange for numbered return clrWhite; //--- Return white default } //+------------------------------------------------------------------+ //| Check if heading | //+------------------------------------------------------------------+ bool IsHeading(string lineText) { if (StringLen(lineText) == 0) return false; //--- False for empty if (StringGetCharacter(lineText, StringLen(lineText) - 1) == ':') return true; //--- True for colon end if (StringFind(lineText, "Canvas Dashboard Usage Guide") >= 0) return true; //--- True for guide if (StringFind(lineText, "Key Features") >= 0) return true; //--- True for features if (StringFind(lineText, "Usage Instructions") >= 0) return true; //--- True for instructions if (StringFind(lineText, "Important Notes") >= 0) return true; //--- True for notes if (StringFind(lineText, "Contact Methods") >= 0) return true; //--- True for contacts if (StringFind(lineText, "NB:") >= 0) return true; //--- True for NB return false; //--- Return false } //+------------------------------------------------------------------+ //| Wrap text | //+------------------------------------------------------------------+ void WrapText(const string inputText, const string font, const int fontSize, const int maxWidth, string &wrappedLines[], color &wrappedColors[]) { ArrayResize(wrappedLines, 0); //--- Reset lines ArrayResize(wrappedColors, 0); //--- Reset colors string paragraphs[]; //--- Declare paragraphs int numParagraphs = StringSplit(inputText, '\n', paragraphs); //--- Split by newline for (int p = 0; p < numParagraphs; p++) { string para = paragraphs[p]; //--- Get paragraph color paraColor = GetLineColor(para); //--- Get color if (StringLen(para) == 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = " "; //--- Add space ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = C'25,25,25'; //--- Set gray continue; //--- Continue } string words[]; //--- Declare words int numWords = StringSplit(para, ' ', words); //--- Split by space string currentLine = ""; //--- Initialize line for (int w = 0; w < numWords; w++) { string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Build test line canvasText.FontSet(font, fontSize); //--- Set font int textW = canvasText.TextWidth(testLine); //--- Get width if (textW <= maxWidth) { currentLine = testLine; //--- Update line } else { if (StringLen(currentLine) > 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = currentLine; //--- Add line ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = paraColor; //--- Set color } currentLine = words[w]; //--- Start new line } } if (StringLen(currentLine) > 0) { int size = ArraySize(wrappedLines); //--- Get size ArrayResize(wrappedLines, size + 1); //--- Resize wrappedLines[size] = currentLine; //--- Add last line ArrayResize(wrappedColors, size + 1); //--- Resize colors wrappedColors[size] = paraColor; //--- Set color } } }
Мы определяем пару вспомогательных функций. Сначала реализуем функцию "LightenColor", которая осветляет цвет, умножая компоненты RGB на коэффициент больше единицы, ограничивая значение до 255 во избежание перетекания, для использования в настройках темы оформления, например, для изменения цвета текста в темном режиме. Функция принимает значение цвета "colorValue" и множитель типа double, извлекает синий/зеленый/красный цвета с помощью маски/сдвигов, умножает каждый на множитель, преобразованный в целое число с помощью MathMin 255, и объединяет с помощью битовых сдвигов/ИЛИ, преобразует в цвет.
Далее мы создаём функцию "TextCalculateSliderHeight" для вычисления размера ползунка полосы прокрутки пропорционально соотношению видимого контента к общему объёму, обеспечивая минимальный размер для удобства использования. Она рассчитывает высоту области как "TextPanelHeight" минус рамки минус двойной размер "text_button_size", устанавливает минимальное значение равным двадцати, возвращает полную область, если нет прокрутки, в противном случае общую площадь области умноженную на видимый коэффициент, возвращает максимальное значение из минимального и значение. Затем определяем функцию "TextScrollUp" для прокрутки текста вверх, уменьшая значение "text_scroll_pos" на единицу "text_adjustedLineHeight", если оно положительно и больше нуля, фиксируем его равным нулю, вызываем функцию "UpdateTextOnCanvas" для перерисовки. Аналогично, "TextScrollDown" прокручивает текст вниз, увеличивая "text_scroll_pos" на высоту одной строки, если она меньше, чем "text_max_scroll", значение которой ограничено значением max, обновляем холст.
Функция "TextUpdateHoverEffects" определяет наведение курсора мыши на части полосы прокрутки на основе локальных координат для обеспечения визуальной обратной связи. Если невидима, завершает работу. Вычисляет положение/размеры полосы прокрутки, проверяет "text_scroll_up_hovered", если на область наведен курсор и находится в пределах верхней части кнопки, "text_scroll_down_hovered", если область находится в нижней части, "text_scroll_slider_hovered" для текущего диапазона положения ползунка, все зависит от "text_scroll_area_hovered". Мы реализуем "GetLineColor", чтобы классифицировать цвета строк по содержанию для стилизации: серый - для пустого пространства/пробела, красный - для электронной почты, фиолетовый / синий - для определенных ссылок / каналов /http/t.me, оранжевый - для нумерованных списков, не определенных фраз, белый - по умолчанию. "IsHeading" определяет заголовки: false - если они пустые, true - если заканчиваются двоеточием или соответствуют таким фразам, как руководство/функциональные возможности/инструкции/примечания/контакты/NB. На самом деле, для идентификации здесь мы могли бы использовать символы HTML, такие как format, но для простоты мы добавили те, которые хотим распознать в функции. Есть много идей, так что выбирайте ту, которая вам подходит.
Наконец, "WrapText" разбивает входной параметр на строки с ограниченной шириной и соответствующими цветами. Сбрасывает выходные массивы, разбивает их новой строкой на абзацы, для каждого получает цвет, добавляет пробел/серый, если он пуст. Разбивает абзацы на слова, создает /тестирует строки: если тест соответствует измеренной ширине (установлен шрифт temp), добавляем; в противном случае дополняем текущую строку цветом, начинаем новую. Добавляет последнюю строку, если она есть. С помощью этих вспомогательных функций мы теперь можем отрисовать главный холст, используя следующую логику.
//+------------------------------------------------------------------+ //| Update text on canvas | //+------------------------------------------------------------------+ void UpdateTextOnCanvas() { canvasText.Erase(0); //--- Clear text canvas int textWidth = canvasText.Width(); //--- Get text width int textHeight = TextPanelHeight; //--- Get text height color text_bg = is_dark_theme ? text_bg_dark : text_bg_light; //--- Get bg color uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert to ARGB canvasText.FillRectangle(0, 0, textWidth - 1, textHeight - 1, argb_bg); //--- Fill background uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border canvasText.Line(0, 0, textWidth - 1, 0, argbBorder); //--- Draw top canvasText.Line(textWidth - 1, 0, textWidth - 1, textHeight - 1, argbBorder); //--- Draw right canvasText.Line(textWidth - 1, textHeight - 1, 0, textHeight - 1, argbBorder); //--- Draw bottom canvasText.Line(0, textHeight - 1, 0, 0, argbBorder); //--- Draw left int padding = 10; //--- Set padding int textAreaX = 1 + padding; //--- Calculate area X int textAreaY = 1; //--- Set area Y int textAreaWidth = textWidth - 2 - padding * 2; //--- Calculate area width int textAreaHeight = textHeight - 2; //--- Calculate area height string font = "Arial"; //--- Set font int fontSize = 16; //--- Set font size canvasText.FontSet(font, fontSize); //--- Set font int lineHeight = canvasText.TextHeight("A"); //--- Get line height text_adjustedLineHeight = lineHeight + 3; //--- Adjust line height text_visible_height = textAreaHeight; //--- Set visible height static string wrappedLines[]; //--- Declare wrapped lines static color wrappedColors[]; //--- Declare wrapped colors static bool wrapped = false; //--- Initialize wrapped flag bool need_scroll = false; //--- Initialize scroll need int reserved_width = 0; //--- Initialize reserved width if (!wrapped) { WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Wrap text wrapped = true; //--- Set wrapped flag } int numLines = ArraySize(wrappedLines); //--- Get number of lines text_total_height = numLines * text_adjustedLineHeight; //--- Calculate total height need_scroll = text_total_height > text_visible_height; //--- Check if scroll needed if (need_scroll) { reserved_width = text_track_width; //--- Set reserved width textAreaWidth -= reserved_width; //--- Adjust area width WrapText(text_usage_text, font, fontSize, textAreaWidth, wrappedLines, wrappedColors); //--- Rewrap text numLines = ArraySize(wrappedLines); //--- Update num lines text_total_height = numLines * text_adjustedLineHeight; //--- Update total height } text_max_scroll = MathMax(0, text_total_height - text_visible_height); //--- Calculate max scroll text_scroll_visible = need_scroll; //--- Set scroll visible text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Adjust scroll pos if (text_scroll_visible) { int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = textHeight - 2; //--- Calculate scrollbar height int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height text_slider_height = TextCalculateSliderHeight(); //--- Calculate slider height int scrollbar_x = textWidth - text_track_width - 1; //--- Calculate scrollbar X color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color uint argb_leader = ColorToARGB(leader_color, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader int slider_y = scrollbar_y + text_button_size + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y if (text_scroll_area_hovered) { color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light; //--- Get button bg color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg color up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg; //--- Determine up bg uint argb_up_bg = ColorToARGB(up_bg, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + text_track_width - 1, scrollbar_y + text_button_size - 1, argb_up_bg); //--- Fill up button color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light; //--- Get arrow color color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Determine up arrow color uint argb_up_arrow = ColorToARGB(up_arrow, 255); //--- Convert to ARGB canvasText.FontSet("Webdings", 22); //--- Set font int arrow_x = scrollbar_x + text_track_width / 2; //--- Calculate arrow X int arrow_y = scrollbar_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x35)) / 2); //--- Calculate arrow Y canvasText.TextOut(arrow_x, arrow_y, CharToString(0x35), argb_up_arrow, TA_CENTER); //--- Draw up arrow int down_y = scrollbar_y + scrollbar_height - text_button_size; //--- Calculate down Y color down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg; //--- Determine down bg uint argb_down_bg = ColorToARGB(down_bg, 255); //--- Convert to ARGB canvasText.FillRectangle(scrollbar_x, down_y, scrollbar_x + text_track_width - 1, down_y + text_button_size - 1, argb_down_bg); //--- Fill down button color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Determine down arrow uint argb_down_arrow = ColorToARGB(down_arrow, 255); //--- Convert to ARGB int down_arrow_x = scrollbar_x + text_track_width / 2; //--- Calculate down arrow X int down_arrow_y = down_y + (text_button_size / 2) - (canvasText.TextHeight(CharToString(0x36)) / 2); //--- Calculate down arrow Y canvasText.TextOut(down_arrow_x, down_arrow_y, CharToString(0x36), argb_down_arrow, TA_CENTER); //--- Draw down arrow int slider_x = scrollbar_x + text_scrollbar_margin; //--- Calculate slider X int slider_w = text_track_width - 2 * text_scrollbar_margin; //--- Calculate slider width int cap_radius = slider_w / 2; //--- Calculate cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine bg uint argb_slider = ColorToARGB(slider_bg, 255); //--- Convert to ARGB canvasText.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc canvasText.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top circle canvasText.Arc(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc canvasText.FillCircle(slider_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom circle canvasText.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle } else { int thin_w = text_scrollbar_thin_width; //--- Set thin width int thin_x = scrollbar_x + (text_track_width - thin_w) / 2; //--- Calculate thin X int cap_radius = thin_w / 2; //--- Calculate cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get bg uint argb_slider = ColorToARGB(slider_bg_color, 255); //--- Convert to ARGB canvasText.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top canvasText.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top canvasText.Arc(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom canvasText.FillCircle(thin_x + cap_radius, slider_y + text_slider_height - cap_radius, cap_radius, argb_slider); //--- Fill bottom canvasText.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + text_slider_height - cap_radius, argb_slider); //--- Fill middle } } color text_base = is_dark_theme ? text_base_dark : text_base_light; //--- Get base color for (int line = 0; line < numLines; line++) { string lineText = wrappedLines[line]; //--- Get line text if (StringLen(lineText) == 0) continue; //--- Skip empty color lineColor = wrappedColors[line]; //--- Get line color if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5); //--- Adjust for dark else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7); //--- Adjust for light int line_y = textAreaY + line * text_adjustedLineHeight - text_scroll_pos; //--- Calculate line Y if (line_y + text_adjustedLineHeight < 0 || line_y > textAreaHeight) continue; //--- Skip out of view if (IsHeading(lineText)) { canvasText.FontSet("Arial Bold", fontSize); //--- Set bold font lineColor = clrDodgerBlue; //--- Set heading color } else canvasText.FontSet(font, fontSize); //--- Set regular font uint argbText = ColorToARGB(lineColor, 255); //--- Convert to ARGB canvasText.TextOut(textAreaX, line_y, lineText, argbText, TA_LEFT); //--- Draw line } canvasText.Update(); //--- Update text canvas }
Здесь мы реализуем функцию "UpdateTextOnCanvas" для отображения прокручиваемого текстового содержимого в объекте "canvasText", обрабатывая фон, рамки, динамическое обтекание, тематический текст с настройками цвета и пользовательскую полосу прокрутки с эффектами при наведении курсора для навигации. Выполняем очистку холста методом Erase, установленным на значение «ноль», получаем текущую ширину/высоту, определяем тематический цвет фона (темный или светлый), преобразуем в ARGB с прозрачностью, равной "TextBackgroundOpacityPercent" (100,0 × 255, преобразованной в uchar), и заполняем прямоугольник от (0,0) до ширина/высота минус один, используя метод FillRectangle. Преобразуем цвет рамки из функции "GetBorderColor" в ARGB с значением 255 и рисуем верхнюю/правую/нижнюю/левую линии с помощью функции Line.
Устанавливаем отступ в десять, вычисляем площадь текста как x один плюс отступ, y один, ширину как общую минус рамки минус двойное отступление, высоту минус рамки. Установим шрифт Arial на шестнадцать с помощью FontSet, получаем высоту строки от символа A с помощью TextHeight, отрегулируем "text_adjustedLineHeight", добавив три для интервала. Устанавливаем "text_visible_height" для высоты области. Если текст не переносится (статическое значение "wrapped" равно false), вызываем функцию "WrapText" с текстом, шрифтом/размером/шириной области для заполнения статических значений "wrappedLines" и "wrappedColors", а затем устанавливаем значение "wrapped" равным true. Получаем количество строк из размера массива, вычисляем "text_total_height" как количество строк, умноженное на скорректированную высоту. Проверяем, требуется ли прокрутка, если общее пространство превышает видимое; если да, установите значение параметра "reserved_width" равным "text_track_width", уменьшаем ширину области, переносим текст, обновляем количество строк/общую высоту.
Рассчитываем "text_max_scroll" как максимальное значение, равное нулю, или общее значение минус видимая область, устанавливаем "text_scroll_visible" равным need_scroll, и ограничиваем значение "text_scroll_pos" в диапазоне от нуля до максимума. Если видимая, вычисляем положение: y — единица, высота — текст минус рамки, высота области минус двойное значение "text_button_size", "text_slider_height" из "TextCalculateSliderHeight", x — ширина минус дорожка минус единица. Получаем главный цвет темы, преобразуем его в ARGB и заполняем прямоугольник главного цвета темы. Вычислите значение ползунка по оси y на основе отношения положения к значению (площадь минус ползунок). Если "text_scroll_area_hovered", получаем тематический фон кнопки/при наведении курсора, определяем верхний фон; если "text_scroll_up_hovered", преобразовываем в ARGB, заполняем прямоугольник кнопки. Получаем цвета стрелок: обычные/отключенные/при наведении курсора, определяем цвет стрелки вверх (отключенная, если позиция равна нулю, иначе — при наведении курсора/обычный режим), преобразовываем, устанавливаем шрифт Webdings в значение двадцать два, вычисляем координаты стрелки по осям x/y с выравниванием по центру, рисуем стрелку вверх '0x35' с помощью TextOut с выравниванием по центру. Аналогично, для кнопки "вниз" в нижней части оси y, фон/ наведение курсора, заливка, цвет стрелки вниз отключен, если положение находится в максимальном значении, в противном случае при наведении курсора / обычный режим, нарисуем '0x36' по центру.
Для ползунка вычисляем x как x плюс margin, w как track минус double margin, ограничиваем радиус половиной w, получаем в теме фона/эффект наведения слайдера, определяем фон при наведении курсора или движении, преобразуем в ARGB. Нарисуем/зальем верхнюю/нижнюю дуги/окружности с помощью функции "Arc"/FillCircle, используя радианы из "DegreesToRadians", зальем средний прямоугольник. В противном случае, если курсор не наведен, нарисуем тонкий ползунок в центре x с небольшой шириной, радиусом закругления в половину, тематическим фоном ARGB, нарисуем /заполним верхнюю/нижнюю дуги / круги, заполним тонкую среднюю часть.
Получаем базовый цвет темы, проходим циклом по "wrappedLines": пропускаем пустые ячейки, получаем/корректируем цвет (если белый/черный, оставляем, иначе осветляем на 1,5 в темном цвете или затемняем на 0,7 в светлом), вычисляем значение y линии как площадь y плюс длина линии, умноженная на скорректированное значение, минус "text_scroll_pos", пропускаем, если линия выходит за пределы видимости (отрицательное значение или выходит за пределы области). Если задан "IsHeading", установим полужирный шрифт и яркий оттенок лазурного цвета (dodger blue); в противном случае — обычный шрифт. Преобразуем в ARGB, нарисуем с выравниванием по левому краю в области x и линии y. Наконец, для отображения текста обновляем холст с помощью Update. Вызываем функцию при инициализации после определения области холста следующим образом.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Other panels logic if (EnableTextPanel) { int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Calculate text Y if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, header_width, TextPanelHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { Print("Failed to create Text Canvas"); //--- Log text creation failure } textCreated = true; //--- Set text created flag } if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text if enabled ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true); //--- Enable mouse wheel events ChartRedraw(); //--- Redraw chart return(INIT_SUCCEEDED); //--- Return success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if (textCreated) canvasText.Destroy(); //--- Destroy text if created ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime lastBarTime = 0; //--- Initialize last time datetime currentBarTime = iTime(_Symbol, _Period, 0); //--- Get current time if (currentBarTime > lastBarTime) { UpdateGraphOnCanvas(); //--- Update graph if (EnableStatsPanel) UpdateStatsOnCanvas(); //--- Update stats if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart lastBarTime = currentBarTime; //--- Update last time } }
Сначала изменяем обработчик OnInit, чтобы включить создание текстовой панели, если значение параметра "EnableTextPanel" равно true. Следуя логике других панелей, мы вычисляем ее положение по y как "currentCanvasY" плюс "header_height", "gap_y", "currentHeight" и "PanelGap", затем создаем графическую метку с CreateBitmapLabel в нулевом подокне, имя "canvasTextName", положение, ширину заголовка, "TextPanelHeight" и COLOR_FORMAT_ARGB_NORMALIZE, выводим сообщение об ошибке в случае сбоя, устанавливаем для "textCreated" значение true в случае успеха. Вызываем "UpdateTextOnCanvas", если включена, для первоначального отображения текста. Включаем события прокрутки колесика мыши с помощью ChartSetInteger, используя CHART_EVENT_MOUSE_WHEEL в значении true, которая понадобится для идентификации перемещения графика при динамической прокрутке, перерисовки с помощью ChartRedraw и возврата INIT_SUCCEEDED.
Затем обновляем обработчик "OnDeinit", чтобы условно уничтожить текстовый холст, если значение "textCreated" равно true, используя "canvasText.Destroy", а затем перерисовываем. Настраиваем обработчик OnTick таким образом, чтобы он включал обновления текстовой панели на новых барах. Используя статическое значение "lastBarTime", равное нулю, получаем время текущего бара с помощью iTime, если оно больше, вызываем "UpdateGraphOnCanvas", опциональную "UpdateStatsOnCanvas" если "EnableStatsPanel", "UpdateTextOnCanvas" если "EnableTextPanel", перерисовываем, обновляем время последнего бара. После компиляции получаем следующий результат.

На изображении видно, что текстовая панель теперь отрисована. Сейчас нам нужно обновить обработчик событий графика, чтобы он обрабатывал события графика для прокрутки текста, который мы отрисовали. Для достижения этого результата мы использовали следующую логику. Мы добавили только те изменения, которые взяты для достижения конечного результата, и исключили другую логику, реализованную нами в предыдущих версиях.
//+------------------------------------------------------------------+ //| Chart event handler | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_CHART_CHANGE) { //--- Existing logic if (EnableTextPanel) UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } else if (id == CHARTEVENT_MOUSE_MOVE) { //--- Existing logic if (EnableTextPanel && !panels_minimized) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE); //--- Get width int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE); //--- Get height bool is_over_text = (mouse_x >= text_canvas_x && mouse_x <= text_canvas_x + text_canvas_w && mouse_y >= text_canvas_y && mouse_y <= text_canvas_y + text_canvas_h); //--- Check over text bool prev_scroll_hovered = text_scroll_area_hovered; //--- Store prev scroll hover text_scroll_area_hovered = false; //--- Reset area hover if (is_over_text) { int local_x = mouse_x - text_canvas_x; //--- Get local X int local_y = mouse_y - text_canvas_y; //--- Get local Y if (local_x >= text_canvas_w - text_track_width - 1) { text_scroll_area_hovered = true; //--- Set area hover } bool prev_up = text_scroll_up_hovered; //--- Store prev up bool prev_down = text_scroll_down_hovered; //--- Store prev down bool prev_slider = text_scroll_slider_hovered; //--- Store prev slider TextUpdateHoverEffects(local_x, local_y); //--- Update hovers if (prev_scroll_hovered != text_scroll_area_hovered || prev_up != text_scroll_up_hovered || prev_down != text_scroll_down_hovered || prev_slider != text_scroll_slider_hovered) { UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } text_mouse_in_body = (local_x < text_canvas_w - text_track_width - 1); //--- Set body flag } else { bool need_redraw = prev_scroll_hovered || text_scroll_up_hovered || text_scroll_down_hovered || text_scroll_slider_hovered; //--- Check redraw need if (need_redraw) { text_scroll_area_hovered = false; //--- Reset area text_scroll_up_hovered = false; //--- Reset up text_scroll_down_hovered = false; //--- Reset down text_scroll_slider_hovered = false; //--- Reset slider UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } text_mouse_in_body = false; //--- Reset body flag } // New: Toggle CHART_MOUSE_SCROLL on enter/leave text body if (text_mouse_in_body != prev_text_mouse_in_body) { ChartSetInteger(0, CHART_MOUSE_SCROLL, !text_mouse_in_body); //--- Toggle scroll prev_text_mouse_in_body = text_mouse_in_body; //--- Update prev } } if (mouse_state == 1 && prev_mouse_state == 0) { //--- Existing logic if (EnableTextPanel && !panels_minimized && text_scroll_visible && text_scroll_area_hovered) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int local_x = mouse_x - text_canvas_x; //--- Get local X int local_y = mouse_y - text_canvas_y; //--- Get local Y int scrollbar_x = canvasText.Width() - text_track_width - 1; //--- Get scrollbar X int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height int scroll_area_y = scrollbar_y + text_button_size; //--- Calculate area Y int scroll_area_height = scrollbar_height - 2 * text_button_size; //--- Calculate area height int slider_y = scroll_area_y + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - text_slider_height)); //--- Calculate slider Y if (local_x >= scrollbar_x && local_x <= scrollbar_x + text_track_width - 1) { if (local_y >= scrollbar_y && local_y <= scrollbar_y + text_button_size - 1) { TextScrollUp(); //--- Scroll up } else if (local_y >= scrollbar_y + scrollbar_height - text_button_size && local_y <= scrollbar_y + scrollbar_height - 1) { TextScrollDown(); //--- Scroll down } else if (local_y >= scroll_area_y && local_y <= scroll_area_y + scroll_area_height - 1) { if (local_y >= slider_y && local_y <= slider_y + text_slider_height - 1) { text_movingStateSlider = true; //--- Set moving slider text_mlbDownY_Slider = local_y; //--- Set down Y text_mlbDown_YD_Slider = slider_y; //--- Set down YD ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable scroll } else { int new_slider_y = local_y - text_slider_height / 2; //--- Calculate new Y new_slider_y = MathMax(scroll_area_y, MathMin(new_slider_y, scroll_area_y + scroll_area_height - text_slider_height)); //--- Clamp Y double ratio = (double)(new_slider_y - scroll_area_y) / (scroll_area_height - text_slider_height); //--- Calculate ratio text_scroll_pos = (int)MathRound(ratio * text_max_scroll); //--- Update pos } UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } } } else if (text_movingStateSlider && mouse_state == 1) { int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int local_y = mouse_y - text_canvas_y; //--- Get local Y int delta_y = local_y - text_mlbDownY_Slider; //--- Calculate delta Y int new_slider_y = text_mlbDown_YD_Slider + delta_y; //--- Calculate new slider Y int scrollbar_y = 1; //--- Set scrollbar Y int scrollbar_height = TextPanelHeight - 2; //--- Get height int slider_min_y = scrollbar_y + text_button_size; //--- Calculate min Y int slider_max_y = scrollbar_y + scrollbar_height - text_button_size - text_slider_height; //--- Calculate max Y new_slider_y = MathMax(slider_min_y, MathMin(new_slider_y, slider_max_y)); //--- Clamp Y double scroll_ratio = (double)(new_slider_y - slider_min_y) / (slider_max_y - slider_min_y); //--- Calculate ratio int new_scroll_pos = (int)MathRound(scroll_ratio * text_max_scroll); //--- Calculate new pos if (new_scroll_pos != text_scroll_pos) { text_scroll_pos = new_scroll_pos; //--- Update pos UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } else if (mouse_state == 0 && prev_mouse_state == 1) { //--- Existing logic if (text_movingStateSlider) { text_movingStateSlider = false; //--- Reset slider moving ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Enable scroll UpdateTextOnCanvas(); //--- Update text ChartRedraw(); //--- Redraw chart } } last_mouse_x = mouse_x; //--- Update last X last_mouse_y = mouse_y; //--- Update last Y prev_mouse_state = mouse_state; //--- Update prev state } else if (id == CHARTEVENT_MOUSE_WHEEL) { int flg_keys = (int)(lparam >> 32); //--- Get keys int mx = (int)(short)lparam; //--- Get X int my = (int)(short)(lparam >> 16); //--- Get Y int delta = (int)dparam; //--- Get delta if (EnableTextPanel && !panels_minimized && text_scroll_visible) { int text_canvas_x = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XDISTANCE); //--- Get text X int text_canvas_y = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YDISTANCE); //--- Get text Y int text_canvas_w = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_XSIZE); //--- Get width int text_canvas_h = (int)ObjectGetInteger(0, canvasTextName, OBJPROP_YSIZE); //--- Get height bool is_over_text_body = (mx >= text_canvas_x && mx <= text_canvas_x + text_canvas_w - text_track_width && my >= text_canvas_y && my <= text_canvas_y + text_canvas_h); //--- Check over body if (is_over_text_body) { int scroll_step = 20; //--- Set step text_scroll_pos += (delta > 0 ? -scroll_step : scroll_step); //--- Adjust pos text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp pos UpdateTextOnCanvas(); //--- Update text int current_scale = (int)ChartGetInteger(0, CHART_SCALE); //--- Get scale int adjust = (delta > 0 ? 1 : -1); // Swap to (delta > 0 ? -1 : 1) if wheel direction is opposite int revert_scale = current_scale + adjust; //--- Calculate revert revert_scale = MathMax(0, MathMin(5, revert_scale)); //--- Clamp scale ChartSetInteger(0, CHART_SCALE, revert_scale); //--- Set scale ChartRedraw(); //--- Redraw chart } } } }
Здесь мы модифицируем обработчик OnChartEvent, чтобы включить интерактивность текстовой панели, расширяя обработку перемещений мыши, если значение "EnableTextPanel" равно true, а не "panels_minimized". Мы извлекаем положение/ширину/высоту текстового холста с помощью ObjectGetInteger для OBJPROP_XDISTANCE/"OBJPROP_YDISTANCE"/"OBJPROP_XSIZE"/"OBJPROP_YSIZE", проверяем, находится ли мышь над всей текстовой областью, сохраняем предыдущее значение "text_scroll_area_hovered" и сбрасываем его в значение false. Если над ней, вычисляем локальные координаты x/ y от положения мыши минус положение холста, если локальная координата x в диапазоне полосы прокрутки (ширина минус "text_track_width" минус единица до конца), установим значение области наведения курсора как true.
Сохраняем предыдущие перемещения вверх / вниз / наведения ползунка, вызываем "TextUpdateHoverEffects" с локальными координатами, чтобы обновить их, если какое-либо наведение или область изменились по сравнению с предыдущими, вызываем "UpdateTextOnCanvas" и перерисовываем. Устанавливаем параметр "text_mouse_in_body" в значение true, если локальная точка x находится слева от полосы прокрутки. В противном случае, если курсор не находится над текстом, но предыдущие наведения указывают на необходимость перерисовки, сбрасываем значения параметров область/верх/низ/наведения ползунка на false, обновляем текст/выполняем перерисовку. Если значение параметра "text_mouse_in_body" изменилось по сравнению с "prev_text_mouse_in_body", переключаем прокрутку графика мышью с помощью ChartSetInteger "CHART_MOUSE_SCROLL" в значение false в теле окна (выход true), чтобы разрешить прокрутку колесиком мыши без масштабирования графика, обновляем предыдущий результат.
При нажатии кнопки мыши (состояние один, предыдущее — ноль), если наведен курсор на текст/прокрутку/область, получаем локальные координаты x/y/полосы прокрутки x/y/высоты/области y/высоты/ползунка y, как и раньше; если локальный x находится в полосе прокрутки: если локальный y при отжатой кнопке, вызываем "TextScrollUp", при нажатой кнопке — "TextScrollDown", иначе, если в области: если у ползунка "text_movingStateSlider" установлена в значение true, сохраняем "text_mlbDownY_Slider" как локальную координату y, "text_mlbDown_YD_Slider" как ползунок y, отключаем прокрутку. В противном случае вычисляем новый ползунок y как локальную координату y минус половина "text_slider_height" ограниченную областью y до области y плюс высота минус "text_slider_height"; соотношение как (новая минус область y) деленное на (высота минус "text_slider_height"), устанавливаем "text_scroll_pos" равной округленному соотношению, умноженному на "text_max_scroll". Затем обновляем текст/перерисовываем.
Если "text_movingStateSlider" имеет значение true и состояние равно единице (удерживается), получаем локальную координату по оси y от мыши минус текст по y, дельта как локальная координата минус "text_mlbDownY_Slider", новая координата ползунка по оси y, как "text_mlbDown_YD_Slider" плюс дельта, ограничиваем значение ползунка минимальным значением по оси y (y плюс "text_button_size") до максимального значения по оси y (y плюс высота минус "text_button_size" минус "text_slider_height"), соотношение как (новое значение минус минимальное) деленное на (максимальное значение минус минимальное), новое положение как округленное соотношение, умноженное на "text_max_scroll", если отличается от текущего значения, обновляем "text_scroll_pos", текст, выполняем перерисовку.
При не нажатой кнопке мыши (состояние ноль, предыдущее), если для параметра "text_movingStateSlider" установлено значение false, включаем прокрутку на значение true, обновляем текст и выполняем перерисовку. В событие изменения графика добавляем необязательный вызов функции "UpdateTextOnCanvas", если "EnableTextPanel" имеет значение true, перед перерисовкой. Для события прокрутки колесика мыши преобразуем "flg_keys" из "lparam" со сдвигом 32, mx из short "lparam", my из short "lparam" со сдвигом 16, delta из "dparam" как целые числа. Если текст/прокрутка видимы, получаем свойства текста, проверяем, не выходит ли он за пределы тела (mx/my по оси x до x плюс ширина минус "text_track_width", по оси y до y плюс высота), если да, то шаг "text_scroll_pos" в двадцать отрицательных/положительных значений при delta >0/<0, ограничиваем ноль значением "text_max_scroll", обновляем текст, получаем текущий масштаб графика с помощью ChartGetInteger "CHART_SCALE", отрегулируем +1/-1 при delta >0/<0 с ограничением 0-5, установим обратно с помощью "ChartSetInteger", чтобы отменить масштабирование, перерисовываем. После компиляции получаем следующий результат.

На изображении видно, что мы улучшили панель на основе холста путем добавления текстовой панели со всеми выполненными взаимодействиями, тем самым достигнув наших целей. Теперь остаётся проверить работоспособность системы, что и рассматривается в следующем разделе.
Тестирование на истории
Мы провели тестирование, а ниже приведена итоговая визуализация в формате Graphics Interchange Format (GIF).

Заключение
В заключение, мы улучшили ценовую панель на основе холста в MQL5, реализовав прокручиваемый текстовый холст с пиксельной точностью для руководств по использованию, обойдя ограничения нативного интерфейса с помощью сглаживания для плавной отрисовки. Также, мы реализовали закругленную пользовательскую полосу прокрутки, которая расширяется при наведении курсора, с кнопками и ползунком для навигации, элементами, отражающими тему оформления, динамическим переносом текста с настройкой цвета и поддержкой функций колесика/кликов/перетаскивания мыши для бесшовного взаимодействия. Система интегрирует текстовую панель с существующими графиками / статистикой, поддерживая функции перетаскивания / изменения размера / оформления / сворачивания и обеспечивает эффективное обновление на основе событий для получения комплексного инструмента мониторинга. Благодаря этому усовершенствованию прокручиваемого текстового холста с пиксельной точностью вы можете предоставлять подробные инструкции для пользователей в пределах дашборда, что позволит далее оптимизировать вашу торговую деятельность. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21072
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Нейросети в трейдинге: Многодоменная архитектура анализа финансовых данных (Основные компоненты)
Оптимизатор ястребов Харриса — Harris Hawks Optimization (HHO)
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования