English Deutsch 日本語
preview
Торговые инструменты на языке MQL5 (Часть 9): Мастер первого запуска для советников с прокручиваемым руководством

Торговые инструменты на языке MQL5 (Часть 9): Мастер первого запуска для советников с прокручиваемым руководством

MetaTrader 5Трейдинг |
65 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

В своей предыдущей статье (Часть 8) мы разработали информационную панель на MetaQuotes Language 5 (MQL5) для мониторинга позиций на нескольких символах и показателей счета. В части 9 мы создаем динамический мастер первого запуска, чтобы ознакомить новых пользователей при первом запуске программы. Мастера настройки при первом запуске являются необходимыми инструментами для упрощения конфигурации сложных систем, такие как советники (EAs) в MetaTrader 5, которые помогают новым пользователям в начальной настройке и предоставляют ориентирующий сценарий для обеспечения оптимального результата. В этой статье мы разрабатываем мастер первоначальной настройки пользователя MQL5 для советников, включающий прокручиваемую панель с динамическим текстом, интерактивные кнопки и флажок для обеспечения упрощенной конфигурации. Запускается только один раз при первом запуске программы. В статье рассмотрим следующие темы:

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

В итоге у вас будет интерактивный мастер в MQL5 для улучшения инициализации советника, который можно адаптировать под ваши торговые задачи. Давайте начнём!


Понимание роли и ценности руководства по первоначальной настройке для торговых программ

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

Наш подход заключается в создании интуитивно понятной панели с возможностью прокрутки, отображающей четкое руководство по настройке с визуально различимым текстом (например, выделенными заголовками и интерактивными ссылками для получения поддержки), интерактивными кнопками для действий пользователя и флажком, позволяющим трейдерам выбирать, пропускать ли руководство в будущих запусках. Мы будем использовать возможности глобальной переменной MQL5 для сохранения выбора пользователя в переменной, которая сохраняет и напоминает номер сборки торгового терминала (версию программного обеспечения) и операционную систему (OS), в которой программа запускается впервые. Мы создадим централизованный интерфейс с заголовком, телом и нижней панелью, включающий динамическое форматирование текста для удобства чтения, прокручиваемый контент для получения подробных инструкций и адаптивный размер для соответствия различным разрешениям экрана. Это гарантирует, что трейдеры смогут легко выполнять такие действия, как настройка параметров риска или включение автоматической торговли, что делает процесс настройки плавным и эффективным. Мы будем следовать встроенной структуре руководства по настройке 'Торговли в один клик' (One Click Trading) следующим образом.

APPROACH FRAMEWORK COMPARISON

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

INITIAL RUN WIZARD FRAMEWORK GIF


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

Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые глобальные переменные, которые будем использовать во всей программе.

//+------------------------------------------------------------------+
//|                                      EA Initialization Setup.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict
#property icon "1. Forex Algo-Trader.ico"

//+------------------------------------------------------------------+
//| Global variables for setup                                       |
//+------------------------------------------------------------------+
string GV_SETUP = "";                             //--- Store global variable name with build and OS
string g_scaled_image_resource = "";              //--- Store name of scaled image resource
int g_mainX = 0;                                  //--- Store calculated x-coordinate of main container
int g_mainY = 50;                                 //--- Set starting y-coordinate 50px below chart top
int g_mainWidth = 500;                            //--- Set main container width
int g_headerHeight = 50;                          //--- Set header height
int g_footerHeight = 40;                          //--- Set footer height
int g_padding = 10;                               //--- Set general padding
int g_textPadding = 10;                           //--- Set text padding
int g_spacing = 0;                                //--- Set spacing between header/body/footer
int g_lineSpacing = 3;                            //--- Set spacing between text lines
int g_minBodyHeight = 200;                        //--- Set minimum body height
int g_maxBodyHeight = 400;                        //--- Set maximum body height
int g_bottomMargin = 50;                          //--- Set bottom margin
int g_displayHeight = 0;                          //--- Store calculated display height
int g_mainHeight = 0;                             //--- Store calculated main container height
int g_adjustedLineHeight = 0;                     //--- Store adjusted line height for scrolling
int g_max_scroll = 0;                             //--- Store maximum scroll to prevent overflow
bool scroll_visible = false;                      //--- Track scrollbar visibility
bool mouse_in_body = false;                       //--- Track if mouse is in body
int scroll_pos = 0;                               //--- Store current scroll position
int prev_scroll_pos = -1;                         //--- Store previous scroll position
int slider_height = 20;                           //--- Set default slider height
bool movingStateSlider = false;                   //--- Track slider drag state
int mlbDownX_Slider = 0;                          //--- Store mouse x on slider click
int mlbDownY_Slider = 0;                          //--- Store mouse y on slider click
int mlbDown_YD_Slider = 0;                        //--- Store slider y on click
int g_total_height = 0;                           //--- Store total text height
int g_visible_height = 0;                         //--- Store visible text height
bool checkbox_checked = false;                    //--- Track checkbox state
bool ok_button_hovered = false;                   //--- Track OK button hover
bool cancel_button_hovered = false;               //--- Track Cancel button hover
bool checkbox_hovered = false;                    //--- Track checkbox hover
bool header_cancel_hovered = false;               //--- Track header cancel hover
bool scroll_up_hovered = false;                   //--- Track scroll up button hover
bool scroll_down_hovered = false;                 //--- Track scroll down button hover
bool scroll_slider_hovered = false;               //--- Track scroll slider hover
string ea_name = "Expert Advisor Setup Wizard";   //--- Set EA name
const int MAX_LINES = 100;                        //--- Set maximum text lines

//+------------------------------------------------------------------+
//| Enum for scrollbar mode                                          |
//+------------------------------------------------------------------+
enum ENUM_SCROLLBAR_MODE {                         // Define scrollbar visibility modes
   SCROLL_ALWAYS,                                  // Show scrollbar if needed
   SCROLL_ON_HOVER,                                // Show scrollbar on hover if needed
   SCROLL_NEVER                                    // Never show scrollbar, wheel only
};
ENUM_SCROLLBAR_MODE ScrollbarMode = SCROLL_ALWAYS; // Scrollbar shows when needed and remains

//+------------------------------------------------------------------+
//| Scrollbar object names                                           |
//+------------------------------------------------------------------+
#define SCROLL_LEADER "Setup_Scroll_Leader"        //--- Define scroll leader name
#define SCROLL_UP_REC "Setup_Scroll_Up_Rec"        //--- Define scroll up rectangle name
#define SCROLL_UP_LABEL "Setup_Scroll_Up_Label"    //--- Define scroll up label name
#define SCROLL_DOWN_REC "Setup_Scroll_Down_Rec"    //--- Define scroll down rectangle name
#define SCROLL_DOWN_LABEL "Setup_Scroll_Down_Label" //--- Define scroll down label name
#define SCROLL_SLIDER "Setup_Scroll_Slider"        //--- Define scroll slider name

//+------------------------------------------------------------------+
//| Dashboard object names                                           |
//+------------------------------------------------------------------+
#define SETUP_MAIN "Setup_MainContainer"           //--- Define main container name
#define SETUP_HEADER_BG "Setup_HeaderBg"           //--- Define header background name
#define SETUP_HEADER_IMAGE "Setup_HeaderImage"     //--- Define header image name
#define SETUP_HEADER_TITLE "Setup_HeaderTitle"     //--- Define header title name
#define SETUP_HEADER_SUBTITLE "Setup_HeaderSubtitle" //--- Define header subtitle name
#define SETUP_HEADER_CANCEL "Setup_HeaderCancel"   //--- Define header cancel button name
#define SETUP_BODY_BG "Setup_BodyBg"               //--- Define body background name
#define SETUP_FOOTER_BG "Setup_FooterBg"           //--- Define footer background name
#define SETUP_CHECKBOX_BG "Setup_CheckboxBg"       //--- Define checkbox background name
#define SETUP_CHECKBOX_LABEL "Setup_CheckboxLabel" //--- Define checkbox label name
#define SETUP_CHECKBOX_TEXT "Setup_CheckboxText"   //--- Define checkbox text name
#define SETUP_OK_BUTTON "Setup_OkButton"           //--- Define OK button name
#define SETUP_CANCEL_BUTTON "Setup_CancelButton"   //--- Define Cancel button name

//+------------------------------------------------------------------+
//| Enhanced setup text                                              |
//+------------------------------------------------------------------+
string setup_text =                                //--- Define setup guide text
"\nExpert Advisor Initialization Guide\n\n"
"Welcome to the Expert Advisor Setup Wizard – Your Gateway to Automated Trading in MetaTrader 5!\n\n"
"Unlock the power of algorithmic trading with this comprehensive setup guide. Designed for seamless integration, this wizard ensures your EA is configured optimally for performance, risk management, and reliability across diverse market conditions.\n\n"
"Key Features:\n"
"- Versatile Configuration: Tailor parameters for lot sizing, magic numbers, stop losses, and take profits to suit your trading style and broker requirements.\n"
"- Risk Controls: Implement drawdown limits, position sizing rules, and equity protection mechanisms to safeguard your capital.\n"
"- Filter Integration: Apply time-based, spread, and news filters to avoid unfavorable trading environments and enhance entry precision.\n"
"- Monitoring Tools: Access real-time panels for trade tracking, performance metrics, and alert notifications.\n"
"- Backtesting Support: Optimize settings with historical data, ensuring robust strategies before live deployment.\n"
"- Broker Adaptability: Supports netting and hedging modes, with customizable slippage and execution tolerances.\n\n"
"Initial Setup Instructions:\n"
"1. Attach the EA to a new chart of your selected symbol (e.g., EURUSD) on an appropriate timeframe (e.g., M15 for intraday strategies).\n"
"2. Adjust core inputs: Define risk parameters, enable/disable filters, and set notification preferences to align with your objectives.\n"
"3. Activate AutoTrading: Ensure MT5's AutoTrading is enabled, and verify EA permissions for secure operation.\n"
"4. Customize Interfaces: Toggle visibility of info panels, trade managers, and alerts for an intuitive user experience.\n"
"5. Validate Setup: Run a forward test on demo to confirm functionality and fine-tune based on observed behavior.\n\n"
"Important Notes:\n"
"- Risk Disclaimer: Automated trading carries inherent risks. Always use appropriate leverage and start with conservative settings on a demo account.\n"
"- Compatibility Check: Confirm broker supports required features like hedging; monitor spreads during volatile periods.\n"
"- Optimization Tips: Regularly review performance logs and adjust filters to adapt to evolving market dynamics.\n"
"- Security Measures: Use unique magic numbers and enable two-factor authentication for account protection.\n"
"- Legal Notice: No guarantees of profitability. Trade responsibly and 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 Expert Advisor solutions. Configure wisely, trade confidently, and elevate your trading journey! 🚀\n";

Чтобы настроить основу мастера, мы используем глобальные переменные для управления структурой панели. Задаем координаты ("g_mainX", "g_mainY" - на 50), размеры ("g_mainWidth" - на 500, "g_headerHeight" - на 50, "g_footerHeight" - на 40) и отступы ("g_padding", "g_textPadding" - на 10). Также определяем интервал ("g_spacing" - на 0, "g_lineSpacing" - на 3), ограничения по высоте тела ("g_minBodyHeight" - на 200, "g_maxBodyHeight" - на 400) и припуск ("g_bottomMargin" - на 50). Для прокрутки устанавливаем такие переменные, как "scroll_visible", "scroll_pos" и "slider_height", равными 20. Состояния взаимодействия с мышью включают в себя "movingStateSlider" и "mlbDownX_Slider". Добавляем флажки при наведении курсора мыши на кнопки и флажки-чекбоксы. Устанавливаем "ea_name" в качестве "Expert Advisor Setup Wizard" (Мастера настройки советника), а "MAX_LINES" - на 100.

Перечисление "ENUM_SCROLLBAR_MODE" определяет поведение полосы прокрутки ("SCROLL_ALWAYS", "SCROLL_ON_HOVER", "SCROLL_NEVER"), значение по умолчанию "SCROLL_ALWAYS". Определяем константы для имен объектов, таких как "SETUP_MAIN", "SETUP_HEADER_BG", "SCROLL_LEADER" и других, для согласованного именования элементов панели и полосы прокрутки. Наконец, создаем строку "setup_text", полное руководство с разделами, посвященными функциям, инструкциям по настройке, заметкам и методам связи, в формате заголовков и пронумерованных шагов, создавая систему организации интерфейса мастера и содержимого для взаимодействия с пользователем. Вы можете изменить расположение или контент; мы просто использовали произвольные значения. Следующее, что нам нужно будет настроить, - это изображение, которое будет использоваться в качестве значка заголовка. Вы можете пропустить этот шаг, если не хотите его выполнять. Нам нужно будет преобразовать наш графический файл в растровый (BMP) файл. После преобразования изображение должно обладать следующими свойствами.

BITMAP FILE IMAGE

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

#resource "1. Forex Algo-Trader SQ.bmp"
#define resourceImg "::1. Forex Algo-Trader SQ.bmp"

Мы используем директиву "#resource", чтобы включить файл изображения с именем "1. Forex Algo-Trader SQ.bmp" и определить константу "resourceImg" как "::1. Forex Algo-Trader SQ.bmp" для ссылки на изображение в программе. Это обеспечит панели мастера профессиональный и фирменный пользовательский интерфейс. Теперь мы приступим к созданию интерфейса, и нам понадобятся некоторые вспомогательные функции. Давайте определим функции для создания необходимых прямоугольных меток, текста, изображений и кнопок.

//+------------------------------------------------------------------+
//| Create rectangle label                                           |
//+------------------------------------------------------------------+
bool createRecLabel(string objName, int xD, int yD, int xS, int yS,
                    color clrBg, int widthBorder, color clrBorder = clrNONE,
                    ENUM_BORDER_TYPE borderType = BORDER_FLAT,
                    ENUM_LINE_STYLE borderStyle = STYLE_SOLID,
                    ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER) {
   ResetLastError();                                              //--- Reset error code
   if (!ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0)) { //--- Create rectangle label
      Print(__FUNCTION__, ": failed to create rec label! Error code = ", GetLastError()); //--- Log failure
      return false;                                               //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD);           //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD);           //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS);               //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS);               //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);          //--- Set corner
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg);          //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, borderType); //--- Set border type
   ObjectSetInteger(0, objName, OBJPROP_STYLE, borderStyle);      //--- Set border style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, widthBorder);      //--- Set border width
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBorder);        //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);             //--- Set to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);            //--- Disable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);       //--- Disable selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);         //--- Disable selection
   return true;                                                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create button                                                    |
//+------------------------------------------------------------------+
bool createButton(string objName, int xD, int yD, int xS, int yS,
                  string txt = "", color clrTxt = clrBlack, int fontSize = 12,
                  color clrBg = clrNONE, color clrBorder = clrNONE,
                  string font = "Arial",
                  ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER, bool isBack = false) {
   ResetLastError();                                              //--- Reset error code
   if (!ObjectCreate(0, objName, OBJ_BUTTON, 0, 0, 0)) {          //--- Create button
      Print(__FUNCTION__, ": failed to create the button! Error code = ", GetLastError()); //--- Log failure
      return false;                                               //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD);           //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD);           //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS);               //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS);               //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);          //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, txt);                //--- Set button text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt);           //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);      //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);               //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrBg);          //--- Set background color
   ObjectSetInteger(0, objName, OBJPROP_BORDER_COLOR, clrBorder); //--- Set border color
   ObjectSetInteger(0, objName, OBJPROP_BACK, isBack);            //--- Set background/foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);            //--- Disable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);       //--- Disable selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);         //--- Disable selection
   return true;                                                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create text label                                                |
//+------------------------------------------------------------------+
bool createLabel(string objName, int xD, int yD,
                 string txt, color clrTxt = clrBlack, int fontSize = 12,
                 string font = "Arial",
                 ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER, ENUM_ANCHOR_POINT anchor = ANCHOR_LEFT_UPPER) {
   ResetLastError();                                              //--- Reset error code
   if (!ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0)) {           //--- Create label
      Print(__FUNCTION__, ": failed to create the label! Error code = ", GetLastError()); //--- Log failure
      return false;                                               //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD);           //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD);           //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);          //--- Set corner
   ObjectSetString(0, objName, OBJPROP_TEXT, txt);                //--- Set label text
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrTxt);           //--- Set text color
   ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);      //--- Set font size
   ObjectSetString(0, objName, OBJPROP_FONT, font);               //--- Set font
   ObjectSetInteger(0, objName, OBJPROP_ANCHOR, anchor);          //--- Set anchor
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);             //--- Set to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);            //--- Disable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);       //--- Disable selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);         //--- Disable selection
   return true;                                                   //--- Return success
}

//+------------------------------------------------------------------+
//| Create bitmap label                                              |
//+------------------------------------------------------------------+
bool createBitmapLabel(string objName, int xD, int yD, int xS, int yS,
                       string bitmapPath, color clr, ENUM_BASE_CORNER corner = CORNER_LEFT_UPPER) {
   ResetLastError();                                              //--- Reset error code
   if (!ObjectCreate(0, objName, OBJ_BITMAP_LABEL, 0, 0, 0)) {    //--- Create bitmap label
      Print(__FUNCTION__, ": failed to create bitmap label! Error code = ", GetLastError()); //--- Log failure
      return false;                                               //--- Return failure
   }
   ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xD);           //--- Set x distance
   ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yD);           //--- Set y distance
   ObjectSetInteger(0, objName, OBJPROP_XSIZE, xS);               //--- Set width
   ObjectSetInteger(0, objName, OBJPROP_YSIZE, yS);               //--- Set height
   ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);          //--- Set corner
   ObjectSetString(0, objName, OBJPROP_BMPFILE, bitmapPath);      //--- Set bitmap path
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);              //--- Set color
   ObjectSetInteger(0, objName, OBJPROP_BACK, false);             //--- Set to foreground
   ObjectSetInteger(0, objName, OBJPROP_STATE, false);            //--- Disable state
   ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);       //--- Disable selectability
   ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);         //--- Disable selection
   return true;                                                   //--- Return success
}

Здесь мы реализуем основные графические компоненты для интерактивной панели. Сначала разрабатываем функцию "createRecLabel", создающую прямоугольную метку (OBJ_RECTANGLE_LABEL) с заданными координатами, размером, цветом фона, шириной границы, типом (BORDER_FLAT), стилем (STYLE_SOLID) и углом (CORNER_LEFT_UPPER), используя функции ObjectCreate и ObjectSetInteger, выводя сообщения об ошибках с помощью Print в случае сбоя при создании, и устанавливая её на невыбираемый передний план. Затем реализуем функцию "createButton", которая создает кнопку (OBJ_BUTTON) с текстом, цветом, размером шрифта, шрифтом (по умолчанию "Arial"), фоном, рамкой и углом, используя тот же формат.

Далее создаем функцию "createLabel", генерирующую текстовую метку (OBJ_LABEL) с текстом, цветом, размером шрифта, шрифтом, углом и привязкой (ANCHOR_LEFT_UPPER), используя аналогичные вызовы для создания объектов и настройки свойств, при необходимости выводя сообщения об ошибках. Наконец, создаем функцию "createBitmapLabel", создающую графическую метку (OBJ_BITMAP_LABEL) для изображений с координатами, размером, путем к растровому изображению, цветом и углом, используя "ObjectCreate" и устанавливая свойства для обеспечения невыбираемого отображения на переднем плане, выводя сообщения о любых сбоях. Это позволит нам создать систему для отображения визуальных элементов мастера, таких как контейнеры, кнопки, текст и изображения.

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

//+------------------------------------------------------------------+
//| Calculate font size based on screen DPI                          |
//+------------------------------------------------------------------+
int getFontSizeByDPI(int baseFontSize, int baseDPI = 96) {
   int currentDPI = (int)TerminalInfoInteger(TERMINAL_SCREEN_DPI); //--- Retrieve current screen DPI
   int scaledFontSize = (int)(baseFontSize * (double)baseDPI / currentDPI); //--- Calculate scaled font size
   return scaledFontSize;                                          //--- Return scaled font size
}

//+------------------------------------------------------------------+
//| Calculate dashboard dimensions                                   |
//+------------------------------------------------------------------+
void CalculateDashboardDimensions() {
   long chart_width = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);   //--- Get chart width
   long chart_height = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS); //--- Get chart height
   g_mainX = (int)((chart_width - g_mainWidth) / 2); //--- Center main container horizontally
   int available_height = (int)(chart_height - g_mainY - g_bottomMargin - g_headerHeight - g_footerHeight - 2 * g_spacing); //--- Calculate available height
   g_displayHeight = MathMin(g_maxBodyHeight, MathMax(g_minBodyHeight, available_height)); //--- Set display height
   g_mainHeight = g_headerHeight + g_displayHeight + g_footerHeight + 2 * g_spacing; //--- Calculate main container height
}

//+------------------------------------------------------------------+
//| Truncate string                                                  |
//+------------------------------------------------------------------+
string truncateString(string valueStr, int startPos, int lengthStr = -1, int threshHold = 0, bool isEllipsis = false) {
   string result = valueStr;                                        //--- Initialize result
   if (StringLen(valueStr) > threshHold && threshHold > 0) {        //--- Check if truncation needed
      result = StringSubstr(valueStr, startPos, lengthStr);         //--- Extract substring
      if (isEllipsis) result += "...";                              //--- Add ellipsis if needed
   }
   return result;                                                   //--- Return truncated string
}

Здесь мы реализуем служебные функции для обеспечения адаптивного изменения размера и форматирования текста. Разрабатываем функцию "getFontSizeByDPI", которая извлекает значение DPI экрана (точек на дюйм), используя TerminalInfoInteger с помощью TERMINAL_SCREEN_DPI, рассчитывает масштабированный размер шрифта, корректируя базовый размер шрифта относительно стандартного значения DPI (96), используя простое соотношение, и возвращает результат для согласованного отображения текста на разных устройствах.

Затем создаем функцию "CalculateDashboardDimensions", которая получает ширину и высоту графика через ChartGetInteger с помощью CHART_WIDTH_IN_PIXELS и "CHART_HEIGHT_IN_PIXELS", центрирует основной контейнер по горизонтали, устанавливая значение "g_mainX" равным половине разницы между шириной графика и "g_mainWidth", вычисляет доступную высоту, вычитая "g_mainY", "g_bottomMargin", "g_headerHeight", "g_footerHeight" и дважды "g_spacing" от высоты графика, устанавливает "g_displayHeight" в пределах границ "g_minBodyHeight" и "g_maxBodyHeight", используя MathMin и MathMax, и вычисляет "g_mainHeight" как сумму высот заголовка, тела и нижнего колонтитула, а также интервала.

Наконец, реализуем функцию "truncateString", которая возвращает строку ввода без изменений, если ее длина меньше порогового значения или равна нулю, в противном случае извлекает подстроку с помощью StringSubstr из "startPos" для символов "lengthStr", добавляя многоточие, если указано, для управления переполнением текста. Используя эти функции, можно начать реализацию с создания главной панели. Для обеспечения модульности мы разместим её логику в отдельной функции.

//+------------------------------------------------------------------+
//| Show the setup dashboard                                         |
//+------------------------------------------------------------------+
void ShowDashboard() {
   checkbox_checked = false;                         //--- Reset checkbox state
   createRecLabel(SETUP_MAIN, g_mainX, g_mainY, g_mainWidth, g_mainHeight, C'20,20,20', 1, C'40,40,40', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create main container
   createRecLabel(SETUP_HEADER_BG, g_mainX, g_mainY, g_mainWidth, g_headerHeight, C'45,45,45', 1, C'60,60,60', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create header background

}

Реализуем функцию "ShowDashboard", и сначала устанавливаем состояние флажка в значение false, обеспечивая тем самым чистый старт для взаимодействия с пользователем. Затем вызываем функцию "createRecLabel", чтобы нарисовать основной контейнер ("SETUP_MAIN") в координатах "g_mainX" и "g_mainY" с размерами "g_mainWidth" и "g_mainHeight", используя темно-серый фон (C'20,20,20'), границу в 1 пиксель (C'40,40,40'), плоский тип рамки, сплошной стиль и выравнивание по верхнему левому углу.

Затем используем "createRecLabel" для создания фона заголовка ("SETUP_HEADER_BG") с той же координатой x и "g_mainY", охватывающей "g_mainWidth" и "g_headerHeight", с немного более светлым серым фоном (C'45,45,45') и рамкой (C'60,60,60'), сохраняя согласованность стиля, отрисовывая базовую визуальную структуру панели мастера. Теперь нам надо вызвать эту функцию в обработчике OnInit 

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   int build = (int)TerminalInfoInteger(TERMINAL_BUILD); //--- Get terminal build number
   string os = TerminalInfoString(TERMINAL_OS_VERSION);  //--- Get operating system version
   StringReplace(os, " ", "_");                          //--- Replace spaces with underscores
   StringReplace(os, ".", "_");                          //--- Replace dots with underscores
   StringReplace(os, ",", "_");                          //--- Replace commas with underscores
   GV_SETUP = "EA_Setup_" + IntegerToString(build) + "_" + os; //--- Set global variable name
   CalculateDashboardDimensions();                       //--- Calculate dashboard dimensions
   if (!GlobalVariableCheck(GV_SETUP)) {                 //--- Check if global variable exists
      Print("Global variable '" + GV_SETUP + "' not found. Creating new one with value FALSE (0.0)."); //--- Log variable creation
      GlobalVariableSet(GV_SETUP, 0.0);                  //--- Set variable to false
      ShowDashboard();                                   //--- Display dashboard
   } else {                                              //--- Variable exists
      double val = GlobalVariableGet(GV_SETUP);          //--- Get variable value
      if (val == 1.0) {                                  //--- Check if set to never show
         // No dashboard
      } else {                                           //--- Show dashboard
         ShowDashboard();                                //--- Display dashboard
      }
   }
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);     //--- Enable mouse move events
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);    //--- Enable mouse wheel events
   ChartSetInteger(0, CHART_MOUSE_SCROLL, true);         //--- Enable chart scrolling
   return(INIT_SUCCEEDED);                               //--- Return initialization success
}

Переходим к реализации логики инициализации для управления поведением отображения в функции OnInit, где получаем номер сборки терминала MetaTrader 5, используя TerminalInfoInteger с помощью TERMINAL_BUILD и версию операционной системы, используя TerminalInfoString с параметром TERMINAL_OS_VERSION, заменяя пробелы, точки и запятые в строке ОС на подчеркивания с помощью StringReplace, чтобы создать чистое имя глобальной переменной ("GV_SETUP"), отформатированное как "EA_Setup_<build>_<OS>". Можно использовать любое другое имя, например, магическое число или имя программы и номер версии, но эта комбинация показалась гениальной и уникальной. Вызываем "CalculateDashboardDimensions" для настройки структуры панели в соответствии с размерами графика.

Затем проверяем, существует ли глобальная переменная, с помощью GlobalVariableCheck; если нет, выводим сообщение о ее создании с помощью "Print", устанавливаем ее значение равным 0.0 (false) с помощью GlobalVariableSet и отображаем панель с помощью "ShowDashboard". Если переменная существует, получаем её значение с помощью GlobalVariableGet, отображая панель только в том случае, если значение не равно 1,0 (что указывает на то, что пользователь решил не показывать её повторно). Наконец, включаем события перемещения мыши и прокрутки колесика мыши с помощью ChartSetInteger, используя CHART_EVENT_MOUSE_MOVE, "CHART_EVENT_MOUSE_WHEEL" и "CHART_MOUSE_SCROLL" для поддержки интерактивных функций, что будет полезно в обработчике OnChartEvent, и возвращаем INIT_SUCCEEDED для успешной инициализации. После компиляции получаем следующий результат.

BASE DASHBOARD WITH HEADER

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

//+------------------------------------------------------------------+
//| Scale image using bicubic interpolation                          |
//+------------------------------------------------------------------+
void ScaleImage(uint &pixels[], int original_width, int original_height, int new_width, int new_height) {
   uint scaled_pixels[];                                               //--- Declare scaled pixel array
   ArrayResize(scaled_pixels, new_width * new_height);                 //--- Resize scaled pixel array
   for (int y = 0; y < new_height; y++) {                              //--- Iterate through new height
      for (int x = 0; x < new_width; x++) {                            //--- Iterate through new width
         double original_x = (double)x * original_width / new_width;   //--- Calculate original x
         double original_y = (double)y * original_height / new_height; //--- Calculate original y
         uint pixel = BicubicInterpolate(pixels, original_width, original_height, original_x, original_y); //--- Interpolate pixel
         scaled_pixels[y * new_width + x] = pixel;                     //--- Store scaled pixel
      }
   }
   ArrayResize(pixels, new_width * new_height);                        //--- Resize original pixel array
   ArrayCopy(pixels, scaled_pixels);                                   //--- Copy scaled pixels
}

//+------------------------------------------------------------------+
//| Perform bicubic interpolation for a single pixel                 |
//+------------------------------------------------------------------+
uint BicubicInterpolate(uint &pixels[], int width, int height, double x, double y) {
   int x0 = (int)x;                                                    //--- Get integer x coordinate
   int y0 = (int)y;                                                    //--- Get integer y coordinate
   double fractional_x = x - x0;                                       //--- Calculate fractional x
   double fractional_y = y - y0;                                       //--- Calculate fractional y
   int x_indices[4], y_indices[4];                                     //--- Declare index arrays
   for (int i = -1; i <= 2; i++) {                                     //--- Calculate indices
      x_indices[i + 1] = MathMin(MathMax(x0 + i, 0), width - 1);       //--- Clamp x indices
      y_indices[i + 1] = MathMin(MathMax(y0 + i, 0), height - 1);      //--- Clamp y indices
   }
   uint neighborhood_pixels[16];                                       //--- Declare neighborhood pixels
   for (int j = 0; j < 4; j++) {                                       //--- Iterate y indices
      for (int i = 0; i < 4; i++) {                                    //--- Iterate x indices
         neighborhood_pixels[j * 4 + i] = pixels[y_indices[j] * width + x_indices[i]]; //--- Store pixel
      }
   }
   uchar alpha_components[16], red_components[16], green_components[16], blue_components[16]; //--- Declare color components
   for (int i = 0; i < 16; i++) {                                      //--- Extract components
      GetArgb(neighborhood_pixels[i], alpha_components[i], red_components[i], green_components[i], blue_components[i]); //--- Get ARGB components
   }
   uchar alpha_out = (uchar)BicubicInterpolateComponent(alpha_components, fractional_x, fractional_y); //--- Interpolate alpha
   uchar red_out = (uchar)BicubicInterpolateComponent(red_components, fractional_x, fractional_y); //--- Interpolate red
   uchar green_out = (uchar)BicubicInterpolateComponent(green_components, fractional_x, fractional_y); //--- Interpolate green
   uchar blue_out = (uchar)BicubicInterpolateComponent(blue_components, fractional_x, fractional_y); //--- Interpolate blue
   return (alpha_out << 24) | (red_out << 16) | (green_out << 8) | blue_out; //--- Combine components
}

//+------------------------------------------------------------------+
//| Perform bicubic interpolation for a single color component       |
//+------------------------------------------------------------------+
double BicubicInterpolateComponent(uchar &components[], double fractional_x, double fractional_y) {
   double weights_x[4];                                                 //--- Declare x weights
   double t = fractional_x;                                             //--- Set x fraction
   weights_x[0] = (-0.5 * t * t * t + t * t - 0.5 * t);                 //--- Calculate x weight 0
   weights_x[1] = (1.5 * t * t * t - 2.5 * t * t + 1);                  //--- Calculate x weight 1
   weights_x[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);             //--- Calculate x weight 2
   weights_x[3] = (0.5 * t * t * t - 0.5 * t * t);                      //--- Calculate x weight 3
   double y_values[4];                                                  //--- Declare y values
   for (int j = 0; j < 4; j++) {                                        //--- Iterate y indices
      y_values[j] = weights_x[0] * components[j * 4 + 0] + weights_x[1] * components[j * 4 + 1] +
                    weights_x[2] * components[j * 4 + 2] + weights_x[3] * components[j * 4 + 3]; //--- Calculate y value
   }
   double weights_y[4];                                                 //--- Declare y weights
   t = fractional_y;                                                    //--- Set y fraction
   weights_y[0] = (-0.5 * t * t * t + t * t - 0.5 * t);                 //--- Calculate y weight 0
   weights_y[1] = (1.5 * t * t * t - 2.5 * t * t + 1);                  //--- Calculate y weight 1
   weights_y[2] = (-1.5 * t * t * t + 2 * t * t + 0.5 * t);             //--- Calculate y weight 2
   weights_y[3] = (0.5 * t * t * t - 0.5 * t * t);                      //--- Calculate y weight 3
   double result = weights_y[0] * y_values[0] + weights_y[1] * y_values[1] +
                   weights_y[2] * y_values[2] + weights_y[3] * y_values[3]; //--- Calculate final value
   return MathMax(0, MathMin(255, result));        //--- Clamp result to valid range
}

//+------------------------------------------------------------------+
//| Extract ARGB components from a pixel                             |
//+------------------------------------------------------------------+
void GetArgb(uint pixel, uchar &alpha, uchar &red, uchar &green, uchar &blue) {
   alpha = (uchar)((pixel >> 24) & 0xFF);                               //--- Extract alpha component
   red = (uchar)((pixel >> 16) & 0xFF);                                 //--- Extract red component
   green = (uchar)((pixel >> 8) & 0xFF);                                //--- Extract green component
   blue = (uchar)(pixel & 0xFF);                                        //--- Extract blue component
}

Здесь мы реализуем функции обработки изображений для повышения визуального качества. Сначала создаем функцию "ScaleImage", которая изменяет размер изображения, создавая новый массив пикселей с помощью ArrayResize для целевых размеров ("new_width" x "new_height"), выполняя перебор каждого пикселя, сопоставляя координаты с исходным изображением с помощью пропорционального масштабирования и вызывая "BicubicInterpolate" для вычисления интерполированного значения пикселя, сохраняя его в масштабированном массиве, а затем копируя обратно в исходный массив с помощью ArrayCopy. 

Затем создаём функцию "BicubicInterpolate", которая вычисляет цвет пикселя в нецелочисленных координатах ("x", "y"), выбирая окрестность пикселей размером 4x4, ограничивая индексы с помощью MathMin и MathMax,  чтобы оставаться в пределах рамки изображения, извлекая компоненты ARGB с помощью "GetArgb", интерполируя каждый компонент с помощью "BicubicInterpolateComponent" и объединяя их в итоговое значение пикселя с помощью побитовых операций. Далее реализуем функцию "BicubicInterpolateComponent", применяющую бикубическую интерполяцию к одному цветовому компоненту, вычисляя кубические веса для дробных координат x и y, вычисляя промежуточные y-значения из сетки 4x4 и комбинируя их с весами y, ограничивая результат значениями от 0 до 255. Наконец, функция "GetArgb" извлекает альфа-, красный, зеленый и синий компоненты из пикселя с помощью побитовых сдвигов и масок. Это позволит адаптировать подход плавного масштабирования наших изображений к приборной панели или выделенной области. Теперь мы можем вызвать эту функцию, чтобы масштабировать исходное изображение и отобразить его.

uint img_pixels[];                                       //--- Declare pixel array for image
uint orig_width = 0, orig_height = 0;                    //--- Initialize image dimensions
bool image_loaded = ResourceReadImage(resourceImg, img_pixels, orig_width, orig_height); //--- Load image resource
if (image_loaded && orig_width > 0 && orig_height > 0) { //--- Check image load success
   ScaleImage(img_pixels, (int)orig_width, (int)orig_height, 40, 40); //--- Scale image to 40x40
   g_scaled_image_resource = "::SetupHeaderImageScaled"; //--- Set scaled image resource name
   if (ResourceCreate(g_scaled_image_resource, img_pixels, 40, 40, 0, 0, 40, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create scaled resource
      createBitmapLabel(SETUP_HEADER_IMAGE, g_mainX + 5, g_mainY + (g_headerHeight - 40)/2, 40, 40, g_scaled_image_resource, clrWhite, CORNER_LEFT_UPPER); //--- Create scaled image label
   } else {                                              //--- Handle resource creation failure
      Print("Failed to create scaled image resource");   //--- Log failure
      createBitmapLabel(SETUP_HEADER_IMAGE, g_mainX + 5, g_mainY + (g_headerHeight - 40)/2, 40, 40, resourceImg, clrWhite, CORNER_LEFT_UPPER); //--- Use original image
   }
} else {                                                 //--- Handle image load failure
   Print("Failed to load original image resource");      //--- Log failure
   createBitmapLabel(SETUP_HEADER_IMAGE, g_mainX + 5, g_mainY + (g_headerHeight - 40)/2, 40, 40, resourceImg, clrWhite, CORNER_LEFT_UPPER); //--- Use original image
}

Чтобы реализовать логику загрузки и масштабирования изображения, объявляем массив пикселей "img_pixels" и инициализируем размеры "orig_width" и "orig_height" равными нулю, затем загружаем исходное изображение с помощью "ResourceReadImage", используя "resourceImg", проверяя, успешно ли это действие ("image_loaded" и размеры > 0); если значение true, вызываем "ScaleImage" для изменения размера до 40x40 пикселей, который вы можете изменить, устанавливаем для "g_scaled_image_resource" значение "::SetupHeaderImageScaled" и создаем новый ресурс с помощью ResourceCreate в формате ARGB, а затем "createBitmapLabel", чтобы отобразить масштабированное изображение в позиции заголовка белым цветом. Если создание ресурса завершается неудачей, выводим сообщение об экземпляре и возвращаемся к исходному изображению; если загрузка завершается неудачей, выводим сообщениев лог и используем исходный "resourceImg" напрямую с помощью функции "createBitmapLabel". После компиляции получаем следующий результат.

HEADER WITH SCALED IMAGE FILE

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

string truncated_name = truncateString(ea_name, 0, -1, 20, true); //--- Truncate EA name
int titleFontSize = getFontSizeByDPI(14);           //--- Calculate title font size
createLabel(SETUP_HEADER_TITLE, g_mainX + 5 + 40 + 5, g_mainY + 5, truncated_name, clrWhite, titleFontSize, "Arial Bold", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create title label
int subtitleFontSize = getFontSizeByDPI(10);        //--- Calculate subtitle font size
createLabel(SETUP_HEADER_SUBTITLE, g_mainX + 5 + 40 + 5, g_mainY + 25, "Streamlined configuration for optimal performance", C'200,200,200', subtitleFontSize, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create subtitle label
int headerCancelFontSize = getFontSizeByDPI(16);    //--- Calculate cancel button font size
createLabel(SETUP_HEADER_CANCEL, g_mainX + g_mainWidth - 25, g_mainY + 10, ShortToString(0x274C), C'150,150,150', headerCancelFontSize, "Arial Rounded MT Bold", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create header cancel button
int bodyY = g_mainY + g_headerHeight + g_spacing;   //--- Calculate body y position
createRecLabel(SETUP_BODY_BG, g_mainX, bodyY, g_mainWidth, g_displayHeight, C'25,25,25', 1, C'40,40,40', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create body background
int footerY = bodyY + g_displayHeight + g_spacing;  //--- Calculate footer y position
createRecLabel(SETUP_FOOTER_BG, g_mainX, footerY, g_mainWidth, g_footerHeight, C'35,35,35', 1, C'50,50,50', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create footer background
createRecLabel(SETUP_CHECKBOX_BG, g_mainX + 10, footerY + (g_footerHeight - 20)/2, 20, 20, C'60,60,60', 1, C'80,80,80', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create checkbox background
int checkboxLabelFontSize = getFontSizeByDPI(17);   //--- Calculate checkbox label font size
createLabel(SETUP_CHECKBOX_LABEL, g_mainX + 10 + 2, footerY + (g_footerHeight - 20)/2, " ", clrWhite, checkboxLabelFontSize, "Wingdings", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create checkbox label
int checkboxTextFontSize = getFontSizeByDPI(10);    //--- Calculate checkbox text font size
createLabel(SETUP_CHECKBOX_TEXT, g_mainX + 40, footerY + (g_footerHeight - 20)/2 + 2, "Do not show this guide again", clrWhite, checkboxTextFontSize, "Calibri Bold", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create checkbox text
int buttonFontSize = getFontSizeByDPI(12);          //--- Calculate button font size
color buttonBg = C'60,60,60';                       //--- Set button background color
color buttonBorder = C'80,80,80';                   //--- Set button border color
createButton(SETUP_OK_BUTTON, g_mainX + g_mainWidth - 170 - 10, footerY + 5, 80, 30, "OK", clrWhite, buttonFontSize, buttonBg, buttonBorder, "Arial Rounded MT Bold", CORNER_LEFT_UPPER, false); //--- Create OK button
createButton(SETUP_CANCEL_BUTTON, g_mainX + g_mainWidth - 80 - 10, footerY + 5, 80, 30, "Cancel", clrWhite, buttonFontSize, buttonBg, buttonBorder, "Arial Rounded MT Bold", CORNER_LEFT_UPPER, false); //--- Create Cancel button
int textFontSize = getFontSizeByDPI(10);            //--- Calculate text font size
for (int i = 0; i < MAX_LINES; i++) {               //--- Create text line labels
   string lineName = "Setup_ResponseLine_" + IntegerToString(i); //--- Generate line name
   createLabel(lineName, 0, -100, " ", clrWhite, textFontSize, "Arial", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create line label
}

Здесь мы реализуем остальные компоненты панели, чтобы мастер завершил работу с пользовательским интерфейсом. Для улучшения читаемости сокращаем название программы до 20 символов с помощью функции "truncateString" и создаём метку заголовка ("SETUP_HEADER_TITLE") с помощью функции "createLabel" в вычислённых координатах, используя размер шрифта, скорректированный по DPI, из функции "getFontSizeByDPI" (основа - 14) и шрифт "Arial Bold". Далее добавляем метку подзаголовка ("SETUP_HEADER_SUBTITLE") с фиксированным текстом и меньшим размером шрифта, скорректированным по DPI (основа - 10), а затем кнопку отмены в заголовке ("SETUP_HEADER_CANCEL"), используя символ крестика Unicode (0x274C) шрифтом "Arial Rounded MT Bold". Вот описание.

CROSS MARK DETAILS

Вычисляем Y-координату тела ("bodyY") и создаем его фон ("SETUP_BODY_BG") с помощью функции "createRecLabel", используя темно-серый фон (C'25,25,25') и рамку, охватывающие "g_mainWidth" и "g_displayHeight". Затем вычисляем Y-координату нижней панели ("footerY") и создаем для него фон ("SETUP_FOOTER_BG") светло-серого цвета, за которым следует фон флажка ("SETUP_CHECKBOX_BG") в виде квадрата 20x20, метка флажка ("SETUP_CHECKBOX_LABEL") с пустым символом Wingdings и текст ("SETUP_CHECKBOX_TEXT"), гласящий "Не показывать это руководство снова" шрифтом Calibri Bold. Добавляем кнопки «ОК» и «Отмена» ("SETUP_OK_BUTTON", "SETUP_CANCEL_BUTTON") с помощью функции "createButton" с размером шрифта, скорректированным по DPI (основа - 12), используя совместимые оттенки серого. Наконец, создаём в цикле в количестве до "MAX_LINES" текстовые метки ("Setup_ResponseLine_") с помощью функции "createLabel", изначально скрытых за пределами экрана, для динамического отображения текста. Это создает систему для отрисовки интерактивной и визуально целостной панели мастера. В результате выполнения программы мы получаем следующее.

WIZARD ENHANCED ELEMENTS

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

//+------------------------------------------------------------------+
//| Get line color based on content                                  |
//+------------------------------------------------------------------+
color GetLineColor(string lineText) {
   if (StringLen(lineText) == 0 || lineText == " ") return C'25,25,25'; //--- Set invisible for empty lines
   if (StringFind(lineText, "mutiiriallan.forex@gmail.com") >= 0) return C'255,100,100'; //--- Set light red for email
   if (StringFind(lineText, "https://t.me/Forex_Algo_Trader") >= 0) return C'150,100,200'; //--- Set light purple for group link
   if (StringFind(lineText, "@ForexAlgo-Trader") >= 0) return C'100,150,255'; //--- Set light blue for channel link
   if (StringFind(lineText, "http") >= 0 || StringFind(lineText, "t.me") >= 0) return C'100,150,255'; //--- Set light blue for general links
   string start3 = StringSubstr(lineText, 0, 3);    //--- Get first three characters
   if ((start3 == "1. " || start3 == "2. " || start3 == "3. " || start3 == "4. " || start3 == "5. ") &&
       StringFind(lineText, "Initial Setup Instructions") < 0) { //--- Check instruction lines
      return C'255,200,100';                        //--- Set light yellow for instructions
   }
   return clrWhite;                                 //--- Default to white
}

//+------------------------------------------------------------------+
//| Wrap text with colors                                            |
//+------------------------------------------------------------------+
void WrapText(const string inputText, const string font, const int fontSize, const int maxWidth, string &wrappedLines[], color &wrappedColors[], int offset = 0) {
   const int maxChars = 60;                         //--- Set maximum characters per line
   ArrayResize(wrappedLines, 0);                    //--- Clear wrapped lines array
   ArrayResize(wrappedColors, 0);                   //--- Clear wrapped colors array
   TextSetFont(font, fontSize);                     //--- Set font
   string paragraphs[];                             //--- Declare paragraphs array
   int numParagraphs = StringSplit(inputText, '\n', paragraphs); //--- Split text into paragraphs
   for (int p = 0; p < numParagraphs; p++) {        //--- Iterate through paragraphs
      string para = paragraphs[p];                  //--- Get current paragraph
      color paraColor = GetLineColor(para);         //--- Get paragraph color
      if (StringLen(para) == 0) {                   //--- Check empty paragraph
         int size = ArraySize(wrappedLines);        //--- Get current size
         ArrayResize(wrappedLines, size + 1);       //--- Resize lines array
         wrappedLines[size] = " ";                  //--- Add empty line
         ArrayResize(wrappedColors, size + 1);      //--- Resize colors array
         wrappedColors[size] = C'25,25,25';         //--- Set invisible color
         continue;                                  //--- Skip to next
      }
      string words[];                               //--- Declare words array
      int numWords = StringSplit(para, ' ', words); //--- Split paragraph into words
      string currentLine = "";                      //--- Initialize current line
      for (int w = 0; w < numWords; w++) {          //--- Iterate through words
         string testLine = currentLine + (StringLen(currentLine) > 0 ? " " : "") + words[w]; //--- Build test line
         uint wid, hei;                             //--- Declare width and height
         TextGetSize(testLine, wid, hei);           //--- Get test line size
         int textWidth = (int)wid;                  //--- Get text width
         if (textWidth + offset <= maxWidth && StringLen(testLine) <= maxChars) { //--- Check line fits
            currentLine = testLine;                 //--- Update current line
         } else {                                   //--- Line exceeds limits
            if (StringLen(currentLine) > 0) {       //--- Check non-empty line
               int size = ArraySize(wrappedLines);  //--- Get current size
               ArrayResize(wrappedLines, size + 1); //--- Resize lines array
               wrappedLines[size] = currentLine;    //--- Add line
               ArrayResize(wrappedColors, size + 1); //--- Resize colors array
               wrappedColors[size] = paraColor;     //--- Add color
            }
            currentLine = words[w];                 //--- Start new line
            TextGetSize(currentLine, wid, hei);     //--- Get new line size
            textWidth = (int)wid;                   //--- Update text width
            if (textWidth + offset > maxWidth || StringLen(currentLine) > maxChars) { //--- Check word too long
               string wrappedWord = "";             //--- Initialize wrapped word
               for (int c = 0; c < StringLen(words[w]); c++) { //--- Iterate through characters
                  string testWord = wrappedWord + StringSubstr(words[w], c, 1); //--- Build test word
                  TextGetSize(testWord, wid, hei);  //--- Get test word size
                  int wordWidth = (int)wid;         //--- Get word width
                  if (wordWidth + offset > maxWidth || StringLen(testWord) > maxChars) { //--- Check word fits
                     if (StringLen(wrappedWord) > 0) { //--- Check non-empty word
                        int size = ArraySize(wrappedLines); //--- Get current size
                        ArrayResize(wrappedLines, size + 1); //--- Resize lines array
                        wrappedLines[size] = wrappedWord; //--- Add wrapped word
                        ArrayResize(wrappedColors, size + 1); //--- Resize colors array
                        wrappedColors[size] = paraColor; //--- Add color
                     }
                     wrappedWord = StringSubstr(words[w], c, 1); //--- Start new word
                  } else {                          //--- Word fits
                     wrappedWord = testWord;        //--- Update wrapped word
                  }
               }
               currentLine = wrappedWord;          //--- Set current line to wrapped word
               if (StringLen(currentLine) > 0) {   //--- Check non-empty line
                  int size = ArraySize(wrappedLines); //--- Get current size
                  ArrayResize(wrappedLines, size + 1); //--- Resize lines array
                  wrappedLines[size] = currentLine; //--- Add line
                  ArrayResize(wrappedColors, size + 1); //--- Resize colors array
                  wrappedColors[size] = paraColor; //--- Add color
               }
               currentLine = "";                   //--- Reset current line
            }
         }
      }
      if (StringLen(currentLine) > 0) {            //--- Check remaining line
         int size = ArraySize(wrappedLines);       //--- Get current size
         ArrayResize(wrappedLines, size + 1);      //--- Resize lines array
         wrappedLines[size] = currentLine;         //--- Add line
         ArrayResize(wrappedColors, size + 1);     //--- Resize colors array
         wrappedColors[size] = paraColor;          //--- Add color
      }
   }
}

Здесь мы применяем логику форматирования текста и цветового кодирования, чтобы улучшить читаемость руководства. В функции "GetLineColor" назначаем цвета в зависимости от содержимого: пустые строки получают невидимый темно-серый цвет (C'25,25,25'), адреса электронной почты — светло-красный (C'255,100,100'), ссылки на группы — светло-фиолетовый (C'150,100,200'), ссылки на авторов и другие унифицированные указатели ресурсов (URL) — светло-голубой (C'100,150,255'), строки инструкций, начинающиеся с "1." до "5." (за исключением заголовка), — светло-желтый (C'255,200,100'), а все остальные по умолчанию — белый. Вы можете задать любой параметр по своему усмотрению; мы включили это только для того, чтобы показать, как можно реализовать расширенное кодирование текста.

В функции "WrapText" разбиваем входной текст на абзацы, используя StringSplit для перевода строк, устанавливаем шрифт с помощью TextSetFont и для каждого абзаца извлекаем его цвет с помощью "GetLineColor", добавляя пустые абзацы в качестве пробелов с невидимым цветом. Разбиваем абзацы на слова с помощью "StringSplit", создавая строки путем добавления слов, если они соответствуют "maxWidth" и ограничению в 60 символов, хотя максимальное ограничение равно 63, используя TextGetSize, в противном случае начинаем новую строку. Для слишком больших слов разбиваем посимвольно, добавляя сегменты к новым строкам если они превышают установленные ограничения, гарантируя, что каждая строка хранится в "wrappedLines", а ее цвет - в "wrappedColors", используя функцию ArrayResize. С помощью этой функции мы можем обновить дисплей. Позаботимся о том, чтобы логика была функцией.

//+------------------------------------------------------------------+
//| Get text height                                                  |
//+------------------------------------------------------------------+
int TextGetHeight(string text, string font, int fontSize) {
   uint wid, hei;                                   //--- Declare width and height
   TextSetFont(font, fontSize);                     //--- Set font
   TextGetSize(text, wid, hei);                     //--- Get text size
   return (int)hei;                                 //--- Return height
}

//+------------------------------------------------------------------+
//| Check if line is a heading                                       |
//+------------------------------------------------------------------+
bool IsHeading(string lineText) {
   if (StringLen(lineText) == 0) return false;      //--- Return false for empty lines
   if (StringGetCharacter(lineText, StringLen(lineText) - 1) == ':') return true; //--- Check for colon
   if (StringFind(lineText, "Expert Advisor Initialization Guide") >= 0) return true; //--- Check main heading
   if (StringFind(lineText, "Key Features") >= 0) return true; //--- Check features heading
   if (StringFind(lineText, "Initial Setup Instructions") >= 0) return true; //--- Check instructions heading
   if (StringFind(lineText, "Important Notes") >= 0) return true; //--- Check notes heading
   if (StringFind(lineText, "Contact Methods") >= 0) return true; //--- Check contact heading
   if (StringFind(lineText, "NB:") >= 0) return true; //--- Check NB heading
   return false;                                    //--- Default to false
}

//+------------------------------------------------------------------+
//| Update body display with scrollable text                         |
//+------------------------------------------------------------------+
void UpdateBodyDisplay() {
   int textX = g_mainX + g_padding + g_textPadding; //--- Set text x position
   int textY = g_mainY + g_headerHeight + g_spacing; //--- Set text y position
   int fullMaxWidth = g_mainWidth - 2 * g_padding - 2 * g_textPadding; //--- Calculate max text width
   string font = "Arial";                           //--- Set font
   int fontSize = getFontSizeByDPI(10);             //--- Calculate font size
   int lineHeight = TextGetHeight("A", font, fontSize); //--- Get line height
   int adjustedLineHeight = lineHeight + g_lineSpacing; //--- Calculate adjusted line height
   g_adjustedLineHeight = adjustedLineHeight;       //--- Store adjusted line height
   int visibleHeight = g_displayHeight;             //--- Set visible height
   g_visible_height = visibleHeight;                //--- Store visible height
   static string wrappedLines[];                    //--- Store wrapped text lines
   static color wrappedColors[];                    //--- Store line colors
   static bool wrapped = false;                     //--- Track if text wrapped
   if (!wrapped) {                                  //--- Check if text needs wrapping
      WrapText(setup_text, font, fontSize, fullMaxWidth, wrappedLines, wrappedColors); //--- Wrap text
      wrapped = true;                               //--- Set wrapped flag
   }
   int numLines = ArraySize(wrappedLines);          //--- Get number of lines
   g_total_height = numLines * adjustedLineHeight;  //--- Calculate total text height
   bool need_scroll = g_total_height > visibleHeight; //--- Check if scrollbar needed
   bool should_show_scrollbar = false;              //--- Initialize scrollbar visibility
   int reserved_width = 0;                          //--- Initialize reserved width
   if (need_scroll && ScrollbarMode != SCROLL_NEVER) { //--- Check scrollbar mode
      should_show_scrollbar = true;                 //--- Enable scrollbar
      reserved_width = 16;                          //--- Reserve scrollbar width
   }
   if (reserved_width > 0 && fullMaxWidth - reserved_width != fullMaxWidth) { //--- Check width change
      WrapText(setup_text, font, fontSize, fullMaxWidth - reserved_width, wrappedLines, wrappedColors); //--- Rewrap text
      numLines = ArraySize(wrappedLines);           //--- Update line count
      g_total_height = numLines * adjustedLineHeight; //--- Update total height
   }

   int startLine = scroll_pos / adjustedLineHeight; //--- Calculate start line
   int currentY = textY;                            //--- Set current y position
   int labelIndex = 0;                              //--- Initialize label index
   for (int line = startLine; line < numLines; line++) { //--- Iterate visible lines
      string lineText = wrappedLines[line];         //--- Get line text
      if (StringLen(lineText) == 0) lineText = " "; //--- Set empty lines to space
      color lineColor = wrappedColors[line];        //--- Get line color
      if (IsHeading(lineText)) lineColor = clrBlue; //--- Set blue for headings
      if (currentY + adjustedLineHeight > textY + visibleHeight) break; //--- Prevent overflow
      string lineName = "Setup_ResponseLine_" + IntegerToString(labelIndex); //--- Generate line name
      if (ObjectFind(0, lineName) >= 0) {          //--- Check if label exists
         ObjectSetString(0, lineName, OBJPROP_TEXT, lineText); //--- Set line text
         ObjectSetInteger(0, lineName, OBJPROP_XDISTANCE, textX); //--- Set x position
         ObjectSetInteger(0, lineName, OBJPROP_YDISTANCE, currentY); //--- Set y position
         ObjectSetInteger(0, lineName, OBJPROP_COLOR, lineColor); //--- Set line color
         string lineFont = IsHeading(lineText) ? "Arial Bold" : "Arial"; //--- Set font
         ObjectSetString(0, lineName, OBJPROP_FONT, lineFont); //--- Set font type
         ObjectSetInteger(0, lineName, OBJPROP_FONTSIZE, fontSize); //--- Set font size
         ObjectSetInteger(0, lineName, OBJPROP_HIDDEN, false); //--- Show label
      }
      currentY += adjustedLineHeight;               //--- Increment y position
      labelIndex++;                                 //--- Increment label index
   }
   for (int i = labelIndex; i < MAX_LINES; i++) {   //--- Hide unused labels
      string lineName = "Setup_ResponseLine_" + IntegerToString(i); //--- Generate line name
      if (ObjectFind(0, lineName) >= 0) {           //--- Check if label exists
         ObjectSetInteger(0, lineName, OBJPROP_HIDDEN, true); //--- Hide label
      }
   }
   ChartRedraw();                                   //--- Redraw chart
}

Чтобы реализовать логику рендеринга текста и прокрутки для динамического отображения руководства, в функции "TextGetHeight" устанавливаем шрифт и размер с помощью TextSetFont и используем TextGetSize для вычисления высоты символа образца, возвращая его для обеспечения единообразного межстрочного интервала. Функция "IsHeading" идентифицирует заголовки, проверяя наличие пустых строк, двоеточий в конце или названий конкретных разделов руководства (например, "Основные функции") и возвращает значение true при совпадении. В функции "UpdateBodyDisplay" рассчитываем положение текстовой области ("textX", "textY") и ширину ("fullMaxWidth"), используя заполнение и размеры контейнера, устанавливаем шрифт Arial с размером, скорректированным по DPI с помощью "getFontSizeByDPI" и вычисляем высоту строки с помощью "TextGetHeight" плюс "g_lineSpacing", сохраняя её в "g_adjustedLineHeight".

Если это еще не сделано, переносим текст руководства с помощью функции "WrapText", вычисляем общую высоту текста ("g_total_height") и определяем видимость полосы прокрутки на основе параметра "ScrollbarMode" и переполнения текста, резервируя при необходимости 16 пикселей для полосы прокрутки и соответствующим образом перенося текст. Вычисляем начальную линию от "scroll_pos", обновляем видимые текстовые метки с помощью ObjectSetString и ObjectSetInteger, определяя их положение, цвет (синий для заголовков с помощью "IsHeading") и шрифт, скрываем неиспользуемые метки и перерисовываем график. При вызове функции внутри функции для отображения панели, получаем следующий результат.

INITIALIZED DISPLAY

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

//+------------------------------------------------------------------+
//| Create scrollbar                                                 |
//+------------------------------------------------------------------+
void CreateScrollbar() {
   int bodyY = g_mainY + g_headerHeight + g_spacing; //--- Calculate body y position
   int textAreaY = bodyY;                            //--- Set text area y
   int textAreaHeight = g_displayHeight;             //--- Set text area height
   int scrollbar_x = g_mainX + g_mainWidth - 16 - 1; //--- Calculate scrollbar x
   int scrollbar_width = 16;                         //--- Set scrollbar width
   int button_size = 16;                             //--- Set button size
   int scrollbar_y = textAreaY + 2;                  //--- Calculate scrollbar y
   int scrollbar_height = textAreaHeight - 2 - 2;    //--- Calculate scrollbar height
   createRecLabel(SCROLL_LEADER, scrollbar_x, scrollbar_y, scrollbar_width, scrollbar_height, C'45,45,45', 1, C'60,60,60', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create scroll leader
   createRecLabel(SCROLL_UP_REC, scrollbar_x, scrollbar_y, scrollbar_width, button_size, C'60,60,60', 1, C'60,60,60', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create scroll up rectangle
   int scrollUpLabelFontSize = getFontSizeByDPI(10); //--- Calculate scroll up font size
   createLabel(SCROLL_UP_LABEL, scrollbar_x + 2, scrollbar_y - 2, CharToString(0x35), C'150,150,150', scrollUpLabelFontSize, "Webdings", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create scroll up label
   int down_rec_y = scrollbar_y + scrollbar_height - button_size; //--- Calculate scroll down y
   createRecLabel(SCROLL_DOWN_REC, scrollbar_x, down_rec_y, scrollbar_width, button_size, C'60,60,60', 1, C'60,60,60', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create scroll down rectangle
   createLabel(SCROLL_DOWN_LABEL, scrollbar_x + 2, down_rec_y - 2, CharToString(0x36), C'150,150,150', scrollUpLabelFontSize, "Webdings", CORNER_LEFT_UPPER, ANCHOR_LEFT_UPPER); //--- Create scroll down label
   slider_height = CalculateSliderHeight();          //--- Calculate slider height
   createRecLabel(SCROLL_SLIDER, scrollbar_x, scrollbar_y + button_size, scrollbar_width, slider_height, C'80,80,80', 1, C'100,100,100', BORDER_FLAT, STYLE_SOLID, CORNER_LEFT_UPPER); //--- Create scroll slider
}

//+------------------------------------------------------------------+
//| Delete scrollbar                                                 |
//+------------------------------------------------------------------+
void DeleteScrollbar() {
   string scroll_objects[] = {SCROLL_LEADER, SCROLL_UP_REC, SCROLL_UP_LABEL, SCROLL_DOWN_REC, SCROLL_DOWN_LABEL, SCROLL_SLIDER}; //--- Define scroll objects
   for (int i = 0; i < ArraySize(scroll_objects); i++) { //--- Iterate through objects
      ObjectDelete(0, scroll_objects[i]);            //--- Delete object
   }
   ChartRedraw();                                    //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Calculate slider height                                          |
//+------------------------------------------------------------------+
int CalculateSliderHeight() {
   int textAreaHeight = g_displayHeight;            //--- Get text area height
   int scroll_area_height = textAreaHeight - 32;    //--- Calculate scroll area height
   int slider_min_height = 20;                      //--- Set minimum slider height
   if (g_total_height <= g_visible_height) return scroll_area_height; //--- Return full height if no scroll
   double visible_ratio = (double)g_visible_height / g_total_height; //--- Calculate visible ratio
   int height = (int)MathFloor(scroll_area_height * visible_ratio); //--- Calculate slider height
   return MathMax(slider_min_height, height);       //--- Return minimum or calculated height
}

//+------------------------------------------------------------------+
//| Update slider position                                           |
//+------------------------------------------------------------------+
void UpdateSliderPosition() {
   int bodyY = g_mainY + g_headerHeight + g_spacing; //--- Calculate body y position
   int textAreaY = bodyY;                           //--- Set text area y
   int textAreaHeight = g_displayHeight;            //--- Set text area height
   int scroll_area_height = textAreaHeight - 32;    //--- Calculate scroll area height
   int slider_min_y = textAreaY + 16;               //--- Set minimum slider y
   if (g_max_scroll <= 0) return;                   //--- Exit if no scroll
   double scroll_ratio = (double)scroll_pos / g_max_scroll; //--- Calculate scroll ratio
   int slider_max_y = slider_min_y + scroll_area_height - slider_height; //--- Calculate max slider y
   int new_y = slider_min_y + (int)MathRound(scroll_ratio * (slider_max_y - slider_min_y)); //--- Calculate new y
   ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y); //--- Set slider y position
}

//+------------------------------------------------------------------+
//| Update scrollbar button colors                                   |
//+------------------------------------------------------------------+
void UpdateButtonColors() {
   int max_scroll = g_max_scroll;                   //--- Get max scroll
   color up_color = (scroll_pos == 0) ? C'80,80,80' : (scroll_up_hovered ? C'100,100,100' : C'150,150,150'); //--- Set up button color
   color down_color = (scroll_pos >= max_scroll) ? C'80,80,80' : (scroll_down_hovered ? C'100,100,100' : C'150,150,150'); //--- Set down button color
   ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, up_color); //--- Update up label color
   ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, down_color); //--- Update down label color
   ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_BGCOLOR, scroll_up_hovered ? C'70,70,70' : C'60,60,60'); //--- Update up rectangle color
   ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_BGCOLOR, scroll_down_hovered ? C'70,70,70' : C'60,60,60'); //--- Update down rectangle color
}

//+------------------------------------------------------------------+
//| Scroll up                                                        |
//+------------------------------------------------------------------+
void ScrollUp() {
   if (g_adjustedLineHeight > 0 && scroll_pos > 0) { //--- Check scroll possible
      scroll_pos = MathMax(0, scroll_pos - g_adjustedLineHeight); //--- Decrease scroll position
      UpdateBodyDisplay();                           //--- Update body display
      if (scroll_visible) {                          //--- Check scrollbar visible
         UpdateSliderPosition();                     //--- Update slider position
         UpdateButtonColors();                       //--- Update button colors
      }
   }
}

//+------------------------------------------------------------------+
//| Scroll down                                                      |
//+------------------------------------------------------------------+
void ScrollDown() {
   int max_scroll = g_max_scroll;                    //--- Get max scroll
   if (g_adjustedLineHeight > 0 && scroll_pos < max_scroll) { //--- Check scroll possible
      scroll_pos = MathMin(max_scroll, scroll_pos + g_adjustedLineHeight); //--- Increase scroll position
      UpdateBodyDisplay();                           //--- Update body display
      if (scroll_visible) {                          //--- Check scrollbar visible
         UpdateSliderPosition();                     //--- Update slider position
         UpdateButtonColors();                       //--- Update button colors
      }
   }
}

Для реализации функциональности полосы прокрутки, обеспечивающей плавную навигацию по руководству по настройке, создаём функцию "CreateScrollbar", в которой рассчитываем положение и размеры полосы прокрутки на основе Y-координаты элемента body ("bodyY"), устанавливая "scrollbar_x" на правый край основного контейнера и используя ширину 16 пикселей, создавая прямоугольник-выноску ("SCROLL_LEADER") с помощью функции "createRecLabel" для дорожки полосы прокрутки, а также кнопки вверх/вниз ("SCROLL_UP_REC", "SCROLL_DOWN_REC") с метками ("SCROLL_UP_LABEL", "SCROLL_DOWN_LABEL") с использованием стрелок Webdings (0x35, 0x36). Вызываем функцию "CalculateSliderHeight", чтобы определить высоту ползунка на основе соотношения видимого текста, и создаем ползунок ("SCROLL_SLIDER") с помощью функции "createRecLabel". Функция "DeleteScrollbar" удаляет все объекты полосы прокрутки ("SCROLL_LEADER" и т. д.) с помощью ObjectDelete и перерисовывает график.

В функции "CalculateSliderHeight" высота ползунка вычисляется как отношение высоты экрана к общей высоте текста, при этом минимальное значение составляет 20 пикселей. Функция "UpdateSliderPosition" регулирует положение ползунка по оси Y, используя коэффициент прокрутки, определяемый параметрами "scroll_pos" и "g_max_scroll", и устанавливает его с помощью функции "ObjectSetInteger". В функции "UpdateButtonColors" обновляем цвета кнопок "вверх"/"вниз" в зависимости от положения прокрутки и состояния при наведении курсора для обеспечения динамической визуальной обратной связи. Функции "ScrollUp" и "ScrollDown" регулируют "scroll_pos" с помощью "g_adjustedLineHeight", ограничивая область прокрутки, и вызывают функции "UpdateBodyDisplay", "UpdateSliderPosition" и "UpdateButtonColors", если полоса прокрутки видна, обеспечивая плавную прокрутку. Теперь мы можем вызвать эти функции внутри функции обновления дисплея, чтобы добавить полосу прокрутки. Вот подход, которую мы используем для достижения этой цели.

int num_visible_lines = g_visible_height / g_adjustedLineHeight; //--- Calculate visible lines
g_max_scroll = MathMax(0, (numLines - num_visible_lines) * g_adjustedLineHeight); //--- Calculate max scroll
bool prev_scroll_visible = scroll_visible;       //--- Store previous scrollbar state
scroll_visible = should_show_scrollbar;          //--- Update scrollbar visibility
if (scroll_visible != prev_scroll_visible) {     //--- Check scrollbar state change
   if (scroll_visible) {                         //--- Show scrollbar
      CreateScrollbar();                         //--- Create scrollbar
   } else {                                      //--- Hide scrollbar
      DeleteScrollbar();                         //--- Delete scrollbar
   }
}
scroll_pos = MathMax(0, MathMin(scroll_pos, g_max_scroll)); //--- Clamp scroll position
if (scroll_visible) {                            //--- Update scrollbar
   slider_height = CalculateSliderHeight();      //--- Calculate slider height
   ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE, slider_height); //--- Set slider height
   UpdateSliderPosition();                       //--- Update slider position
   UpdateButtonColors();                         //--- Update button colors
}

Внутри функции "UpdateBodyDisplay" вычисляем количество видимых строк, деля "g_visible_height" на "g_adjustedLineHeight", и определяем максимальное расстояние прокрутки ("g_max_scroll") как избыточную высоту текста за пределами видимых строк, используя MathMax для предотвращения отрицательных значений. Храним предыдущее состояние видимости полосы прокрутки в переменной "prev_scroll_visible", обновляем "scroll_visible" в зависимости от того, нужна ли полоса прокрутки, и если состояние изменяется, вызываем "CreateScrollbar" для отрисовки полосы прокрутки или "DeleteScrollbar" для ее удаления. Ограничиваем значение параметра "scroll_pos" в диапазоне от 0 до "g_max_scroll" с помощью функций "MathMax" и MathMin для предотвращения переполнения. Если полоса прокрутки видна, обновляем значение параметра "slider_height" с помощью "CalculateSliderHeight", устанавливаем высоту ползунка с помощью ObjectSetInteger для параметра "SCROLL_SLIDER" и вызываем функции "UpdateSliderPosition" и "UpdateButtonColors", чтобы обновить внешний вид и положение полосы прокрутки. При компилировании получаем следующий результат.

SCROLLBAR ENABLED

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

//+------------------------------------------------------------------+
//| Delete the dashboard                                             |
//+------------------------------------------------------------------+
void DeleteDashboard() {
   string objects[] = {                             //--- Define dashboard objects
      SETUP_MAIN, SETUP_HEADER_BG, SETUP_HEADER_IMAGE, SETUP_HEADER_TITLE, SETUP_HEADER_SUBTITLE, SETUP_HEADER_CANCEL,
      SETUP_BODY_BG, SETUP_FOOTER_BG, SETUP_CHECKBOX_BG, SETUP_CHECKBOX_LABEL, SETUP_CHECKBOX_TEXT,
      SETUP_OK_BUTTON, SETUP_CANCEL_BUTTON, SCROLL_LEADER, SCROLL_UP_REC, SCROLL_UP_LABEL,
      SCROLL_DOWN_REC, SCROLL_DOWN_LABEL, SCROLL_SLIDER
   };
   for (int i = 0; i < ArraySize(objects); i++) {   //--- Iterate through objects
      ObjectDelete(0, objects[i]);                  //--- Delete object
   }
   int total = ObjectsTotal(0);                     //--- Get total objects
   for (int j = total - 1; j >= 0; j--) {           //--- Iterate through remaining objects
      string name = ObjectName(0, j);               //--- Get object name
      if (StringFind(name, "Setup_ResponseLine_") == 0) { //--- Check for text lines
         ObjectDelete(0, name);                     //--- Delete text line
      }
   }
   ChartRedraw();                                   //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   DeleteDashboard();                                //--- Remove dashboard objects
   if (StringLen(g_scaled_image_resource) > 0) {     //--- Check if scaled image exists
      ResourceFree(g_scaled_image_resource);         //--- Free scaled image resource
   }
}

Здесь мы реализуем функцию очистки для мастера, чтобы обеспечить надлежащее управление ресурсами. В функции "DeleteDashboard" определяем массив имен объектов панели, включая компоненты основного контейнера, заголовка, тела, нижней панели, кнопок, флажка и полосы прокрутки, и перебираем их с помощью ObjectDelete, чтобы удалить каждый из них с графика. Затем мы выполняем перебор в цикле по всем оставшимся объектам графика с помощью ObjectsTotal и ObjectName, удаляя все объекты текстовых строк, начинающиеся с "Setup_ResponseLine_", используя "ObjectDelete", и перерисовываем график с помощью ChartRedraw для получения чистого экрана. В "OnDeinit" вызываем функцию "DeleteDashboard", чтобы удалить все элементы панели и проверить, существует ли масштабированный ресурс изображения ("StringLen(g_scaled_image_resource) > 0"), освобождая его с помощью ResourceFree для освобождения памяти. Теперь можно перейти к оживлению панели. Нам надо сделать это так, чтобы при нажатии на кнопки они выполняли свои назначенные вызовы, а эффекты при наведения курсора отображали состояние курсора. Для простоты нам понадобится функция, позволяющая это сделать.

//+------------------------------------------------------------------+
//| Update hover effects                                             |
//+------------------------------------------------------------------+
void UpdateHoverEffects(int mouseX, int mouseY) {
   int ok_x = (int)ObjectGetInteger(0, SETUP_OK_BUTTON, OBJPROP_XDISTANCE);    //--- Get OK button x
   int ok_y = (int)ObjectGetInteger(0, SETUP_OK_BUTTON, OBJPROP_YDISTANCE);    //--- Get OK button y
   int ok_width = (int)ObjectGetInteger(0, SETUP_OK_BUTTON, OBJPROP_XSIZE);    //--- Get OK button width
   int ok_height = (int)ObjectGetInteger(0, SETUP_OK_BUTTON, OBJPROP_YSIZE);   //--- Get OK button height
   bool is_ok_hovered = (mouseX >= ok_x && mouseX <= ok_x + ok_width && mouseY >= ok_y && mouseY <= ok_y + ok_height); //--- Check OK button hover
   if (is_ok_hovered != ok_button_hovered) {                                   //--- Check hover state change
      ok_button_hovered = is_ok_hovered;                                       //--- Update hover state
      color hoverBg = is_ok_hovered ? C'40,80,40' : C'60,60,60';               //--- Set hover background
      color hoverBorder = is_ok_hovered ? C'60,100,60' : C'80,80,80';          //--- Set hover border
      ObjectSetInteger(0, SETUP_OK_BUTTON, OBJPROP_BGCOLOR, hoverBg);          //--- Update background
      ObjectSetInteger(0, SETUP_OK_BUTTON, OBJPROP_BORDER_COLOR, hoverBorder); //--- Update border
      ChartRedraw();                                                           //--- Redraw chart
   }
   int cancel_x = (int)ObjectGetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_XDISTANCE);  //--- Get Cancel button x
   int cancel_y = (int)ObjectGetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_YDISTANCE);  //--- Get Cancel button y
   int cancel_width = (int)ObjectGetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_XSIZE);  //--- Get Cancel button width
   int cancel_height = (int)ObjectGetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_YSIZE); //--- Get Cancel button height
   bool is_cancel_hovered = (mouseX >= cancel_x && mouseX <= cancel_x + cancel_width && mouseY >= cancel_y && mouseY <= cancel_y + cancel_height); //--- Check Cancel button hover
   if (is_cancel_hovered != cancel_button_hovered) {                                 //--- Check hover state change
      cancel_button_hovered = is_cancel_hovered;                                     //--- Update hover state
      color hoverBg = is_cancel_hovered ? C'80,40,40' : C'60,60,60';                 //--- Set hover background
      color hoverBorder = is_cancel_hovered ? C'100,60,60' : C'80,80,80';            //--- Set hover border
      ObjectSetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_BGCOLOR, hoverBg);            //--- Update background
      ObjectSetInteger(0, SETUP_CANCEL_BUTTON, OBJPROP_BORDER_COLOR, hoverBorder);   //--- Update border
      ChartRedraw();                               //--- Redraw chart
   }
   int checkbox_x = (int)ObjectGetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_XDISTANCE);  //--- Get checkbox x
   int checkbox_y = (int)ObjectGetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_YDISTANCE);  //--- Get checkbox y
   int checkbox_width = (int)ObjectGetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_XSIZE);  //--- Get checkbox width
   int checkbox_height = (int)ObjectGetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_YSIZE); //--- Get checkbox height
   bool is_checkbox_hovered = (mouseX >= checkbox_x && mouseX <= checkbox_x + checkbox_width && mouseY >= checkbox_y && mouseY <= checkbox_y + checkbox_height); //--- Check checkbox hover
   if (is_checkbox_hovered != checkbox_hovered) {   //--- Check hover state change
      checkbox_hovered = is_checkbox_hovered;       //--- Update hover state
      if (checkbox_checked) {                       //--- Check checkbox state
         ObjectSetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_BGCOLOR, is_checkbox_hovered ? C'0,150,0' : C'0,128,0'); //--- Update background
      } else {                                      //--- Unchecked state
         ObjectSetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_BGCOLOR, is_checkbox_hovered ? C'70,70,70' : C'60,60,60'); //--- Update background
      }
      ChartRedraw();                                //--- Redraw chart
   }
   int header_cancel_x = (int)ObjectGetInteger(0, SETUP_HEADER_CANCEL, OBJPROP_XDISTANCE); //--- Get header cancel x
   int header_cancel_y = (int)ObjectGetInteger(0, SETUP_HEADER_CANCEL, OBJPROP_YDISTANCE); //--- Get header cancel y
   int header_cancel_width = 20;                    //--- Set header cancel width
   int header_cancel_height = 20;                   //--- Set header cancel height
   bool is_header_cancel_hovered = (mouseX >= header_cancel_x && mouseX <= header_cancel_x + header_cancel_width &&
                                    mouseY >= header_cancel_y && mouseY <= header_cancel_y + header_cancel_height); //--- Check header cancel hover
   if (is_header_cancel_hovered != header_cancel_hovered) { //--- Check hover state change
      header_cancel_hovered = is_header_cancel_hovered; //--- Update hover state
      ObjectSetInteger(0, SETUP_HEADER_CANCEL, OBJPROP_COLOR, is_header_cancel_hovered ? C'255,100,100' : C'150,150,150'); //--- Update color
      ChartRedraw();                                //--- Redraw chart
   }
   if (scroll_visible) {                            //--- Check scrollbar visible
      int scroll_up_x = (int)ObjectGetInteger(0, SCROLL_UP_REC, OBJPROP_XDISTANCE);  //--- Get scroll up x
      int scroll_up_y = (int)ObjectGetInteger(0, SCROLL_UP_REC, OBJPROP_YDISTANCE);  //--- Get scroll up y
      int scroll_up_width = (int)ObjectGetInteger(0, SCROLL_UP_REC, OBJPROP_XSIZE);  //--- Get scroll up width
      int scroll_up_height = (int)ObjectGetInteger(0, SCROLL_UP_REC, OBJPROP_YSIZE); //--- Get scroll up height
      bool is_scroll_up_hovered = (mouseX >= scroll_up_x && mouseX <= scroll_up_x + scroll_up_width &&
                                   mouseY >= scroll_up_y && mouseY <= scroll_up_y + scroll_up_height); //--- Check scroll up hover
      if (is_scroll_up_hovered != scroll_up_hovered) {                               //--- Check hover state change
         scroll_up_hovered = is_scroll_up_hovered;                                   //--- Update hover state
         ObjectSetInteger(0, SCROLL_UP_REC, OBJPROP_BGCOLOR, is_scroll_up_hovered ? C'70,70,70' : C'60,60,60'); //--- Update background
         ObjectSetInteger(0, SCROLL_UP_LABEL, OBJPROP_COLOR, (scroll_pos == 0) ? C'80,80,80' : (is_scroll_up_hovered ? C'100,100,100' : C'150,150,150')); //--- Update label color
         ChartRedraw();                                                              //--- Redraw chart
      }
      int scroll_down_x = (int)ObjectGetInteger(0, SCROLL_DOWN_REC, OBJPROP_XDISTANCE);  //--- Get scroll down x
      int scroll_down_y = (int)ObjectGetInteger(0, SCROLL_DOWN_REC, OBJPROP_YDISTANCE);  //--- Get scroll down y
      int scroll_down_width = (int)ObjectGetInteger(0, SCROLL_DOWN_REC, OBJPROP_XSIZE);  //--- Get scroll down width
      int scroll_down_height = (int)ObjectGetInteger(0, SCROLL_DOWN_REC, OBJPROP_YSIZE); //--- Get scroll down height
      bool is_scroll_down_hovered = (mouseX >= scroll_down_x && mouseX <= scroll_down_x + scroll_down_width &&
                                     mouseY >= scroll_down_y && mouseY <= scroll_down_y + scroll_down_height); //--- Check scroll down hover
      if (is_scroll_down_hovered != scroll_down_hovered) {                               //--- Check hover state change
         scroll_down_hovered = is_scroll_down_hovered;                                   //--- Update hover state
         ObjectSetInteger(0, SCROLL_DOWN_REC, OBJPROP_BGCOLOR, is_scroll_down_hovered ? C'70,70,70' : C'60,60,60'); //--- Update background
         ObjectSetInteger(0, SCROLL_DOWN_LABEL, OBJPROP_COLOR, (scroll_pos >= g_max_scroll) ? C'80,80,80' : (is_scroll_down_hovered ? C'100,100,100' : C'150,150,150')); //--- Update label color
         ChartRedraw();                                                                  //--- Redraw chart
      }
      int scroll_slider_x = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE);  //--- Get scroll slider x
      int scroll_slider_y = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE);  //--- Get scroll slider y
      int scroll_slider_width = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);  //--- Get scroll slider width
      int scroll_slider_height = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE); //--- Get scroll slider height
      bool is_scroll_slider_hovered = (mouseX >= scroll_slider_x && mouseX <= scroll_slider_x + scroll_slider_width &&
                                       mouseY >= scroll_slider_y && mouseY <= scroll_slider_y + scroll_slider_height); //--- Check scroll slider hover
      if (is_scroll_slider_hovered != scroll_slider_hovered) {                           //--- Check hover state change
         scroll_slider_hovered = is_scroll_slider_hovered;                               //--- Update hover state
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, is_scroll_slider_hovered ? C'100,100,100' : C'80,80,80'); //--- Update background
         ChartRedraw();                                                                  //--- Redraw chart
      }
   }
}

Здесь мы реализуем эффекты при наведении курсора мыши на интерактивные элементы, созданные нами для улучшения обратной связи с пользователем. Создаем функцию "UpdateHoverEffects" и в ней проверяем координаты мыши ("mouseX", "mouseY") на соответствие положениям и размерам кнопки OK ("SETUP_OK_BUTTON"), кнопки отмены ("SETUP_CANCEL_BUTTON"), флажка ("SETUP_CHECKBOX_BG"), кнопки отмены заголовка ("SETUP_HEADER_CANCEL") и компонентов полосы прокрутки ("SCROLL_UP_REC", "SCROLL_DOWN_REC", "SCROLL_SLIDER"), использующих ObjectGetInteger для своих размеров.

Для каждого элемента определяем наведение курсора, проверяя, находится ли мышь в пределах его границ, обновляя соответствующие состояния при наведении курсора ("ok_button_hovered", "cancel_button_hovered" и т.д.), если они изменены, и корректируем цвета с помощью ObjectSetInteger: Кнопка «ОК» использует зеленые оттенки (C'40,80,40'), кнопка «Отмена» — красные оттенки (C'80,40,40'), флажок — зеленый цвет, если установлен (C'0,150,0'), или серый цвет (C'70,70,70'), отмена заголовка переключается на ярко-красный цвет (C'255,100,100'), а кнопки/ползунок полосы прокрутки используют различные оттенки серого (C'70,70,70' или C'100,100,100') в зависимости от положения при наведении курсора и прокрутки ("scroll_pos"). Все эти цвета можно изменить по вашему вкусу. Мы просто снова использовали произвольные значения, чтобы получить преимущество перед визуальной обратной связью. Перерисовываем график с помощью ChartRedraw после каждого обновления. Теперь нам нужно просто реализовать это в обработчике OnChartEvent, который обрабатывает все события на графике, но в нашем случае нас интересуют события перемещения мыши и щелчка.

//+------------------------------------------------------------------+
//| Chart event handler                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) {
   int mouseX = (int)lparam;                        //--- Get mouse x coordinate
   int mouseY = (int)dparam;                        //--- Get mouse y coordinate
   int body_inner_y = g_mainY + g_headerHeight + g_spacing; //--- Calculate body y position
   int textAreaEventY = body_inner_y;               //--- Set text area y position
   int textAreaEventH = g_displayHeight;            //--- Set text area height
   int bodyX = g_mainX + g_padding + g_textPadding; //--- Calculate body x position
   int bodyW = g_mainWidth - 2 * g_padding - 2 * g_textPadding - (scroll_visible ? 16 : 0); //--- Calculate body width
   if (id == CHARTEVENT_OBJECT_CLICK) {             //--- Handle object click events
      if (sparam == SETUP_HEADER_CANCEL || sparam == SETUP_CANCEL_BUTTON) { //--- Check cancel button click
         GlobalVariableSet(GV_SETUP, 0.0);          //--- Set global variable to false
         DeleteDashboard();                         //--- Remove dashboard from chart
      } else if (sparam == SETUP_OK_BUTTON) {       //--- Check OK button click
         double new_val = checkbox_checked ? 1.0 : 0.0; //--- Set global variable based on checkbox
         GlobalVariableSet(GV_SETUP, new_val);      //--- Update global variable
         DeleteDashboard();                         //--- Remove dashboard from chart
      } else if (sparam == SETUP_CHECKBOX_BG || sparam == SETUP_CHECKBOX_TEXT || sparam == SETUP_CHECKBOX_LABEL) { //--- Check checkbox click
         checkbox_checked = !checkbox_checked;      //--- Toggle checkbox state
         string check_text = checkbox_checked ? CharToString(252) : " ";         //--- Set checkbox symbol
         ObjectSetString(0, SETUP_CHECKBOX_LABEL, OBJPROP_TEXT, check_text);     //--- Update checkbox label text
         color text_color = checkbox_checked ? C'173,216,230' : clrWhite;        //--- Set text color based on state
         ObjectSetInteger(0, SETUP_CHECKBOX_TEXT, OBJPROP_COLOR, text_color);    //--- Update checkbox text color
         if (checkbox_checked) {                    //--- Check if checkbox is selected
            ObjectSetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_BGCOLOR, C'0,128,0'); //--- Set checked background color
            ObjectSetInteger(0, SETUP_CHECKBOX_LABEL, OBJPROP_COLOR, clrWhite);  //--- Set checked label color
         } else {                                   //--- Handle unchecked state
            ObjectSetInteger(0, SETUP_CHECKBOX_BG, OBJPROP_BGCOLOR, C'60,60,60'); //--- Set unchecked background color
            ObjectSetInteger(0, SETUP_CHECKBOX_LABEL, OBJPROP_COLOR, clrWhite);  //--- Set unchecked label color
         }
         ChartRedraw();                            //--- Redraw chart to reflect changes
      } else if (sparam == SCROLL_UP_REC || sparam == SCROLL_UP_LABEL) {         //--- Check scroll up click
         ScrollUp();                               //--- Execute scroll up action
      } else if (sparam == SCROLL_DOWN_REC || sparam == SCROLL_DOWN_LABEL) {     //--- Check scroll down click
         ScrollDown();                             //--- Execute scroll down action
      }
   } else if (id == CHARTEVENT_MOUSE_MOVE) {       //--- Handle mouse move events
      int MouseState = (int)sparam;                //--- Get mouse state
      bool is_in = (mouseX >= bodyX && mouseX <= bodyX + bodyW && mouseY >= textAreaEventY && mouseY <= textAreaEventY + textAreaEventH); //--- Check if mouse is in body
      mouse_in_body = is_in;                       //--- Update mouse in body status
      UpdateHoverEffects(mouseX, mouseY);          //--- Update hover effects for elements
      static int prevMouseState = 0;               //--- Store previous mouse state
      if (prevMouseState == 0 && MouseState == 1 && scroll_visible) {                //--- Check for slider drag start
         int xd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XDISTANCE); //--- Get slider x position
         int yd_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE); //--- Get slider y position
         int xs_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_XSIZE);     //--- Get slider width
         int ys_slider = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YSIZE);     //--- Get slider height
         if (mouseX >= xd_slider && mouseX <= xd_slider + xs_slider &&
             mouseY >= yd_slider && mouseY <= yd_slider + ys_slider) { //--- Check if mouse is over slider
            movingStateSlider = true;              //--- Set slider drag state
            mlbDownX_Slider = mouseX;              //--- Store mouse x position
            mlbDownY_Slider = mouseY;              //--- Store mouse y position
            mlbDown_YD_Slider = yd_slider;         //--- Store slider y position
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, C'100,100,100'); //--- Set drag color
            ChartSetInteger(0, CHART_MOUSE_SCROLL, false); //--- Disable chart scrolling
         }
      }
      if (movingStateSlider) {                     //--- Handle slider dragging
         int delta_y = mouseY - mlbDownY_Slider;   //--- Calculate y displacement
         int new_y = mlbDown_YD_Slider + delta_y;  //--- Calculate new slider y position
         int textAreaY_local = body_inner_y;       //--- Set text area y position
         int textAreaHeight_local = g_displayHeight;   //--- Set text area height
         int scroll_area_y_min = textAreaY_local + 16; //--- Set minimum slider y
         int scroll_area_y_max = textAreaY_local + textAreaHeight_local - 16 - slider_height; //--- Set maximum slider y
         new_y = MathMax(scroll_area_y_min, MathMin(new_y, scroll_area_y_max));               //--- Clamp new y position
         ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE, new_y);                        //--- Update slider y position
         int max_scroll = g_max_scroll;            //--- Get maximum scroll
         double scroll_ratio = (double)(new_y - scroll_area_y_min) / (scroll_area_y_max - scroll_area_y_min); //--- Calculate scroll ratio
         int new_scroll_pos = (int)MathRound(scroll_ratio * max_scroll);                      //--- Calculate new scroll position
         if (new_scroll_pos != scroll_pos) {       //--- Check if scroll position changed
            scroll_pos = new_scroll_pos;           //--- Update scroll position
            UpdateBodyDisplay();                   //--- Update body text display
         }
         ChartRedraw();                            //--- Redraw chart
      }
      if (MouseState == 0) {                       //--- Handle mouse release
         if (movingStateSlider) {                  //--- Check if slider was being dragged
            movingStateSlider = false;             //--- Reset drag state
            int max_scroll = g_max_scroll;         //--- Get maximum scroll
            int textAreaY_local = body_inner_y;    //--- Set text area y position
            int textAreaHeight_local = g_displayHeight; //--- Set text area height
            int scroll_area_y_min_local = textAreaY_local + 16; //--- Set minimum slider y
            int scroll_area_y_max_local = textAreaY_local + textAreaHeight_local - 16 - slider_height; //--- Set maximum slider y
            int current_slider_y = (int)ObjectGetInteger(0, SCROLL_SLIDER, OBJPROP_YDISTANCE); //--- Get current slider y
            double scroll_ratio = (double)(current_slider_y - scroll_area_y_min_local) / (scroll_area_y_max_local - scroll_area_y_min_local); //--- Calculate scroll ratio
            int temp_scroll = (int)MathRound(scroll_ratio * max_scroll); //--- Calculate temporary scroll
            if (g_adjustedLineHeight > 0) {        //--- Check if line height valid
               int snapped_line = (int)MathRound((double)temp_scroll / g_adjustedLineHeight); //--- Calculate snapped line
               scroll_pos = MathMax(0, MathMin(snapped_line * g_adjustedLineHeight, max_scroll)); //--- Snap to nearest line
            } else {                               //--- No valid line height
               scroll_pos = temp_scroll;           //--- Use temporary scroll
            }
            UpdateBodyDisplay();                   //--- Update body text display
            UpdateSliderPosition();                //--- Update slider position
            ObjectSetInteger(0, SCROLL_SLIDER, OBJPROP_BGCOLOR, scroll_slider_hovered ? C'100,100,100' : C'80,80,80'); //--- Reset slider color
            ChartSetInteger(0, CHART_MOUSE_SCROLL, true); //--- Re-enable chart scrolling
         }
      }
      prevMouseState = MouseState;                 //--- Update previous mouse state
   } else if (id == CHARTEVENT_MOUSE_WHEEL) {      //--- Handle mouse wheel events
      int wheel_delta = (int)sparam;               //--- Get wheel delta
      bool in_body = (mouseX >= bodyX && mouseX <= bodyX + bodyW && mouseY >= textAreaEventY && mouseY <= textAreaEventY + textAreaEventH); //--- Check if mouse in body
      if (in_body && g_total_height > g_visible_height && g_adjustedLineHeight > 0) { //--- Check scroll conditions
         int direction = (wheel_delta > 0) ? -1 : 1; //--- Determine scroll direction
         int notches = MathAbs(wheel_delta) / 120; //--- Calculate scroll notches
         int scroll_amount = g_adjustedLineHeight * direction * notches; //--- Calculate scroll amount
         scroll_pos += scroll_amount;              //--- Update scroll position
         int max_scroll = g_max_scroll;            //--- Get maximum scroll
         scroll_pos = MathMax(0, MathMin(scroll_pos, max_scroll)); //--- Clamp scroll position
         UpdateBodyDisplay();                      //--- Update body text display
         if (scroll_visible) {                     //--- Check if scrollbar visible
            UpdateSliderPosition();                //--- Update slider position
            UpdateButtonColors();                  //--- Update button colors
         }
         ChartRedraw();                            //--- Redraw chart
      }
   }
}

Наконец, в функции OnChartEvent обрабатываем события клика (CHARTEVENT_OBJECT_CLICK), проверяя объект, на который был совершен клик ("sparam"): если это отмена заголовка ("SETUP_HEADER_CANCEL") или кнопка отмены ("SETUP_CANCEL_BUTTON"), устанавливаем глобальную переменную "GV_SETUP" в значение 0.0 с помощью GlobalVariableSet и удаляем панель с помощью "DeleteDashboard"; если это кнопка OK ("SETUP_OK_BUTTON"), устанавливаем "GV_SETUP" в значение 1.0, если установлен флажок ("checkbox_checked"), или в значение 0.0 в противном случае, затем удаляем панель. Если это компоненты флажка ("SETUP_CHECKBOX_BG", "SETUP_CHECKBOX_TEXT", "SETUP_CHECKBOX_LABEL"), переключаем состояние "checkbox_checked", обновляем метку флажка на галочку (Unicode 252) или пробел с помощью ObjectSetString, устанавливаем цвет текста на светло-голубой (C'173,216,230') или белый с помощью ObjectSetInteger и настраиваем фон флажка на зеленый (C'0,128,0') или серый (C'60,60,60'). MQL5 предоставляет подробный список символов шрифта Wingdings, и именно его мы и использовали. Вы можете использовать любой из предложенных вариантов или другой подход. Вот визуализация возможных символов MQL5 Wingdings, которые вы можете использовать.

MQL5 WINGDINGS

Для кликов прокрутки вверх ("SCROLL_UP_REC", "SCROLL_UP_LABEL") или вниз ("SCROLL_DOWN_REC", "SCROLL_DOWN_LABEL") вызываем "ScrollUp" или "ScrollDown". Для событий перемещения мыши (CHARTEVENT_MOUSE_MOVE) рассчитываем область тела, обновляем "mouse_in_body" и вызываем "UpdateHoverEffects" с помощью "mouseX" и "mouseY"; обнаруживаем начало перетаскивания ползунка ("MouseState" 1), проверяя, находится ли мышь над "SCROLL_SLIDER", устанавливая "movingStateSlider" и сохраняя положения мыши, и во время перетаскивания корректируем положение ползунка по оси y с помощью ObjectSetInteger, обновляем "scroll_pos" на основе коэффициента прокрутки и обновляем дисплей с помощью "UpdateBodyDisplay". При отпускании кнопки мыши ("MouseState" 0) привязываем "scroll_pos" к ближайшей строке, сбрасываем состояние ползунка и снова включаем прокрутку графика с помощью функции ChartSetInteger. Для событий, связанных с колесиком мыши (CHARTEVENT_MOUSE_WHEEL), корректируем "scroll_pos" на основе направления колесика и выемок, фиксируем его в пределах "g_max_scroll" и обновляем отображение и полосу прокрутки. При компилировании получаем следующий результат.

FINAL WIZARD WITH FULL FUNCTIONALITY

На изображении видно, что панель теперь полностью функциональна и интерактивна. При нажатии на кнопку "Ок" мы должны получить установленные глобальные переменные. Чтобы получить доступ к глобальной переменной, необходимо нажать "Tools" ("Сервис"), затем "Global Variables" ("Глобальные переменные") или просто нажать клавишу F3 на клавиатуре. Ниже приведена полная визуализация.

GLOBAL VARIABLE SETUP

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


Тестирование мастера начальной настройки

Мы провели тестирование, а ниже представлена скомпилированная визуализация в едином формате растрового изображения Graphics Interchange Format (GIF).

WIZARD BACKTEST GIF


Заключение

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

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

Прикрепленные файлы |
Знакомство с языком MQL5 (Часть 38): Освоение API и функции WebRequest в языке MQL5 (XII) Знакомство с языком MQL5 (Часть 38): Освоение API и функции WebRequest в языке MQL5 (XII)
Создайте практический мост между MetaTrader 5 и Binance: получайте 30-минутные свечи с помощью WebRequest, извлекайте из JSON значения OHLC и времени и подтверждайте бычий паттерн поглощения, используя только полностью закрытые свечи. Затем соберите строку запроса, вычислите подпись HMAC-SHA256, добавьте X-MBX-APIKEY и отправьте аутентифицированные ордера. Вы получите четкий сквозной рабочий процесс советника – от получения данных до исполнения ордера.
Искусство работы с логами (Часть 8): Самопереводящиеся записи об ошибках Искусство работы с логами (Часть 8): Самопереводящиеся записи об ошибках
В этой восьмой части серии «Мастерство работы с логами» мы исследуем реализацию многоязычных сообщений об ошибках в Logify — мощной библиотеке логирования для MQL5. Вы узнаете, как структурировать ошибки с контекстом, переводить сообщения на несколько языков и динамически форматировать логи по уровням серьезности. И всё это — с чистым, расширяемым и готовым к продакшену дизайном.
Возможности Мастера MQL5, которые вам нужно знать (Часть 71): Использование паттернов MACD и OBV Возможности Мастера MQL5, которые вам нужно знать (Часть 71): Использование паттернов MACD и OBV
Осциллятор схождения-расхождения скользящих средних (Moving-Average-Convergence-Divergence, MACD) и индикатор балансового объема (On-Balance-Volume, OBV) - еще одна пара индикаторов, которые можно использовать совместно в советнике MQL5. Как это принято в данной серии статей, данная комбинация индикаторов дополняет друг друга: MACD подтверждает тренды, а OBV проверяет объем. Как обычно, мы используем Мастер MQL5 для построения паттернов и тестирования потенциала, который может иметь эта пара индикаторов.
Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2) Переосмысливаем классические стратегии (Часть 13): Обновление стратегии по пересечению скользящих (Часть 2)
Мы попробуем внедрить дополнительные улучшения в нашу стратегию по пересечению скользящих средних, чтобы постараться снизить задержку и повысить надежность за счет дополнительного анализа данных. Как мы знаем, проецирование данных в многомерное пространство иногда может улучшить производительность моделей машинного обучения. Давайте посмотрим, что это на практике означает для нас, трейдеров. Также увидим, как можно использовать этот эффективный принцип в терминале MetaTrader 5.