English
preview
Торговые инструменты MQL5 (Часть 33): Создание системы документации с расширенным форматированием для программ на языке MQL5

Торговые инструменты MQL5 (Часть 33): Создание системы документации с расширенным форматированием для программ на языке MQL5

MetaTrader 5 — Торговые системы |
30 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Введение

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

Часть 33 непосредственно развивает эти наработки. Здесь мы идём гораздо дальше, с нуля переосмысливая подход к документации с помощью класса CCanvas. Это позволяет создать систему документации с расширенным форматированием, полностью отрисовываемую на холсте. Она поддерживает вкладки, форматирование фрагментов текста с помощью разметки, встроенные изображения, тематические палитры, отрисовку со сглаживанием методом суперсэмплинга и прокручиваемую область содержимого. Эти возможности позволяют разработчикам MQL5 и алгоритмическим трейдерам размещать автономную документацию непосредственно внутри своих программ. Мы рассмотрим следующие темы:

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

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


Новый подход к документации, встроенной в график: концепция и дизайн

Документация всегда находилась вне самого инструмента. Отдельный PDF-файл, внешняя ссылка, тема на форуме — всё это пользователю приходится искать самостоятельно, но чаще всего он этого так и не делает. Идея проста: разместить документацию с форматированием и интерактивными элементами непосредственно в самой программе, чтобы она была доступна при подключении программы к графику. Представьте себе руководство пользователя или руководство с расширенным форматированием — документ, который вы обычно открываете в программе для просмотра PDF-файлов или текстовом процессоре, но здесь он встроен непосредственно в MetaTrader 5 и связан с той самой программой, которую описывает.

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

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

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

АРХИТЕКТУРА СИСТЕМЫ ДОКУМЕНТАЦИИ С РАСШИРЕННЫМ ФОРМАТИРОВАНИЕМ


Реализация на языке MQL5

Определение основных структур, перечислений и предварительных объявлений

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

//+------------------------------------------------------------------+
//|                                        Rich Content Document.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict
#property description "Rich Content Document. A canvas-rendered, PDF-like documentation"
#property description "system demonstrating MQL5's full rich content capabilities."
#property description "Scrollable. Formatted. Beautiful. Right inside your MT5 chart."

#include <Canvas/Canvas.mqh>

//+------------------------------------------------------------------+
//| Resources                                                        |
//+------------------------------------------------------------------+
#resource "Rich Content Document - Logo.bmp"           // Embedded logo bitmap
#resource "Rich Content Document - MQL5 IDE.bmp"       // MQL5 IDE screenshot
#resource "Rich Content Document - MT5 Chart.bmp"      // MT5 chart screenshot
#resource "Rich Content Document - Strategy Tester.bmp"// Strategy Tester screenshot
#resource "Rich Content Document - Market Watch.bmp"   // Market Watch screenshot
#resource "Rich Content Document - Navigator.bmp"      // Navigator screenshot

//+------------------------------------------------------------------+
//| Resource path macros                                             |
//+------------------------------------------------------------------+
#define RCD_LOGO_RESOURCE     "::Rich Content Document - Logo.bmp"           // Logo resource string
#define RCD_IMG_MQL5_IDE      "::Rich Content Document - MQL5 IDE.bmp"       // MQL5 IDE resource string
#define RCD_IMG_MT5_CHART     "::Rich Content Document - MT5 Chart.bmp"      // MT5 Chart resource string
#define RCD_IMG_STRAT_TESTER  "::Rich Content Document - Strategy Tester.bmp"// Strategy Tester resource string
#define RCD_IMG_MARKET_WATCH  "::Rich Content Document - Market Watch.bmp"   // Market Watch resource string
#define RCD_IMG_NAVIGATOR     "::Rich Content Document - Navigator.bmp"      // Navigator resource string

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
input group "RICH CONTENT DOCUMENT SETTINGS"
input int  rcdTheme        = 1;    // Theme: 0=Dark, 1=Light
input bool rcdShowOnAttach = true; // Show document on attach

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
#define RCD_SS                 4        // Supersampling factor for AA rendering

#define RCD_HEADER_H           48       // Header height in pixels
#define RCD_TABS_H             36       // Tabs bar height in pixels
#define RCD_FOOTER_H           50       // Footer height in pixels
#define RCD_GAP                0        // Gap between sections

#define RCD_PAD                16       // Body text padding
#define RCD_LOGO_SIZE          32       // Logo display size in header

#define RCD_MIN_WIDTH          520      // Minimum panel width
#define RCD_MAX_WIDTH          720      // Maximum panel width
#define RCD_MIN_BODY_H         160      // Minimum body height
#define RCD_MAX_BODY_H         540      // Maximum body height
#define RCD_TOP_MARGIN         40       // Distance from top of chart
#define RCD_SIDE_MARGIN        40       // Distance from sides of chart
#define RCD_BOTTOM_MARGIN      40       // Distance from bottom of chart

#define RCD_SB_PILL_W          5        // Scrollbar pill width
#define RCD_SB_MARGIN_R        4        // Scrollbar right margin

#define RCD_CHECKBOX_SIZE      16       // Checkbox size in pixels
#define RCD_BUTTON_W           90       // Footer button width
#define RCD_BUTTON_H           30       // Footer button height

#define RCD_FONT_TITLE         11       // Header title font size
#define RCD_FONT_SUBTITLE      9        // Header subtitle font size
#define RCD_FONT_TAB           9        // Tab label font size
#define RCD_FONT_BODY          11       // Body text font size
#define RCD_FONT_HEADING       11       // Section heading font size
#define RCD_FONT_BUTTON        9        // Button label font size
#define RCD_FONT_LABEL         9        // Checkbox label font size
#define RCD_FONT_CLOSE         13       // Close button font size

#define RCD_LINE_GAP           5        // Extra pixels between text lines
#define RCD_TOP_PAD_BODY       10       // Top padding inside body area

#define RCD_IMG_COUNT          5        // Total number of content images
#define RCD_IMG_MQL5IDE_IDX    0        // Image index: MQL5 IDE
#define RCD_IMG_MT5CHART_IDX   1        // Image index: MT5 Chart
#define RCD_IMG_STTEST_IDX     2        // Image index: Strategy Tester
#define RCD_IMG_MWATCH_IDX     3        // Image index: Market Watch
#define RCD_IMG_NAV_IDX        4        // Image index: Navigator

#define RCD_TAB_COUNT          5        // Total number of tabs
#define RCD_TAB_LANGUAGE       0        // Tab index: MQL5 Language
#define RCD_TAB_METAEDITOR     1        // Tab index: MetaEditor
#define RCD_TAB_PLATFORM       2        // Tab index: MT5 Platform
#define RCD_TAB_TESTER         3        // Tab index: Strategy Tester
#define RCD_TAB_RESOURCES      4        // Tab index: Resources

#define RCD_IMG_LINE_PREFIX    "RCDIMG:"  // Prefix for image placeholder lines
#define RCD_LOGO_LINE_PREFIX   "RCDLOGO:" // Prefix for logo placeholder lines

//+------------------------------------------------------------------+
//| Paragraph type enum                                              |
//+------------------------------------------------------------------+
enum RcdParaType
  {
   RCD_PARA_EMPTY    = 0,  // Blank spacer line
   RCD_PARA_BODY     = 1,  // Plain body text
   RCD_PARA_HEADING  = 2,  // Section heading
   RCD_PARA_NUMBERED = 3,  // Numbered list item
   RCD_PARA_WARN     = 5,  // Warning block (orange/red bar)
   RCD_PARA_INFO     = 7,  // Info block (blue bar)
   RCD_PARA_ANSWER   = 9,  // Answer/tip block (green bar)
   RCD_PARA_IMG      = 11, // Image placeholder
   RCD_PARA_LOGO     = 12, // Logo placeholder
   RCD_PARA_BULLET   = 13  // Bullet list item
  };

//+------------------------------------------------------------------+
//| Inline run structure — styled text segment within a paragraph    |
//+------------------------------------------------------------------+
struct RcdRun
  {
   string text;          // Actual text content
   color  col;           // Text color (0 = inherit paragraph default)
   color  bgCol;         // Background highlight color (0 = none)
   bool   bold;          // Bold style flag
   bool   italic;        // Italic style flag
   bool   underline;     // Underline style flag
   bool   strikethrough; // Strikethrough style flag
  };

//+------------------------------------------------------------------+
//| Paragraph structure — single content unit                        |
//+------------------------------------------------------------------+
struct RcdPara
  {
   RcdParaType type;   // Paragraph type
   string      text;   // Paragraph text content
   int         imgIdx; // Image index (only for RCD_PARA_IMG)
  };

//+------------------------------------------------------------------+
//| Canvas object name strings                                       |
//+------------------------------------------------------------------+
string rcdHeaderCanvasName  = "RCD_Header";        // Header canvas object name
string rcdTabsCanvasName    = "RCD_Tabs";          // Tabs canvas object name
string rcdBodyCanvasName    = "RCD_Body";          // Body canvas object name
string rcdBodyHRCanvasName  = "RCD_Body_HR";       // Body high-res canvas name
string rcdBlockCanvasName   = "RCD_Block";         // Block overlay canvas name
string rcdFooterCanvasName  = "RCD_Footer";        // Footer canvas object name
string rcdLogoScaledResName = "::RCD_Logo_Scaled"; // Scaled logo resource name

//+------------------------------------------------------------------+
//| Canvas instances                                                 |
//+------------------------------------------------------------------+
CCanvas rcdCanvHeader;   // Header canvas
CCanvas rcdCanvTabs;     // Tabs canvas
CCanvas rcdCanvBody;     // Body canvas
CCanvas rcdCanvBodyHR;   // Body high-resolution supersampled canvas
CCanvas rcdCanvBlock;    // Block overlay canvas (text + images + scrollbar)
CCanvas rcdCanvFooter;   // Footer canvas

//+------------------------------------------------------------------+
//| Per-tab paragraph arrays                                         |
//+------------------------------------------------------------------+
RcdPara rcdTabParasLanguage[];   // MQL5 Language tab paragraphs
RcdPara rcdTabParasMetaEditor[]; // MetaEditor tab paragraphs
RcdPara rcdTabParasPlatform[];   // MT5 Platform tab paragraphs
RcdPara rcdTabParasTester[];     // Strategy Tester tab paragraphs
RcdPara rcdTabParasResources[];  // Resources tab paragraphs

//+------------------------------------------------------------------+
//| State variables                                                  |
//+------------------------------------------------------------------+
bool rcdIsActive     = false; // Document is currently displayed
bool rcdContentBuilt = false; // Content arrays have been populated
bool rcdLogoLoaded   = false; // Header logo was successfully loaded

//--- Layout geometry
int rcdPanelX      = 0; // Panel left edge X coordinate
int rcdPanelY      = 0; // Panel top edge Y coordinate
int rcdPanelWidth  = 0; // Panel total width
int rcdBodyHeight  = 0; // Body section height
int rcdTotalHeight = 0; // Total panel height
int rcdHeaderY     = 0; // Header top Y coordinate
int rcdTabsY       = 0; // Tabs bar top Y coordinate
int rcdBodyY       = 0; // Body top Y coordinate
int rcdFooterY     = 0; // Footer top Y coordinate

//--- Tab and content state
int     rcdActiveTab          = RCD_TAB_LANGUAGE; // Currently selected tab index
string  rcdTabTitles[RCD_TAB_COUNT];              // Display names for each tab
RcdPara rcdCurrentParas[];                        // Working copy of active tab paragraphs
string  rcdWrappedLines[];                        // Wrapped text lines for rendering
int     rcdLineHeight         = 0;                // Pixel height of one text line
int     rcdTotalContentHeight = 0;                // Total pixel height of all lines

//--- Scroll state
int  rcdScrollPos          = 0;     // Current scroll offset in pixels
int  rcdMaxScroll          = 0;     // Maximum allowed scroll position
int  rcdSliderHeight       = 0;     // Computed scrollbar pill height
bool rcdScrollVisible      = false; // Whether scrollbar is shown
bool rcdIsDraggingSlider   = false; // Slider drag in progress
int  rcdDragStartMouseY    = 0;     // Mouse Y when drag started
int  rcdDragStartScrollPos = 0;     // Scroll position when drag started

//--- Hover state flags
bool rcdHoverClose    = false;           // Mouse over close button
bool rcdHoverOK       = false;           // Mouse over OK button
bool rcdHoverCancel   = false;           // Mouse over Cancel button
bool rcdHoverCheckbox = false;           // Mouse over checkbox
bool rcdHoverSlider   = false;           // Mouse over scrollbar slider
bool rcdHoverTabs[RCD_TAB_COUNT];        // Mouse over each tab
bool rcdMouseInBody   = false;           // Mouse is inside body area
bool rcdPrevMouseInBody  = false;        // Previous mouse-in-body state
int  rcdPrevMouseState   = 0;            // Previous mouse button state

//--- Checkbox state
bool rcdDontShowAgain = false; // User checked "don't show again"

//--- Image cache arrays (5 images x flat pixel storage)
int  rcdImgOrigW[RCD_IMG_COUNT];         // Original image widths
int  rcdImgOrigH[RCD_IMG_COUNT];         // Original image heights
int  rcdImgScaledW[RCD_IMG_COUNT];       // Scaled image widths
int  rcdImgScaledH[RCD_IMG_COUNT];       // Scaled image heights
bool rcdImgLoaded[RCD_IMG_COUNT];        // Image was loaded from resource
bool rcdImgCacheValid[RCD_IMG_COUNT];    // Scaled cache is current
int  rcdImgCacheForWidth[RCD_IMG_COUNT]; // Panel width when cache was built
uint rcdImgPixels0[];                    // Pixel data for image 0 (MQL5 IDE)
uint rcdImgPixels1[];                    // Pixel data for image 1 (MT5 Chart)
uint rcdImgPixels2[];                    // Pixel data for image 2 (Strategy Tester)
uint rcdImgPixels3[];                    // Pixel data for image 3 (Market Watch)
uint rcdImgPixels4[];                    // Pixel data for image 4 (Navigator)

//--- Logo display (large version used in Resources tab)
uint rcdLogoDisplayPixels[];  // Scaled logo pixel data for body display
int  rcdLogoDisplayW    = 0;  // Logo display width
int  rcdLogoDisplayH    = 0;  // Logo display height
bool rcdLogoDisplayReady = false; // Logo display data is ready

//+------------------------------------------------------------------+
//| Theme color variables                                            |
//+------------------------------------------------------------------+
color rcdBg;               // Main background color
color rcdPanelAlt;         // Alternate panel color (footer)
color rcdHeaderBg;         // Header background color
color rcdTabsBg;           // Tabs bar background color
color rcdBorder;           // Border color
color rcdHeaderText;       // Header title text color
color rcdSubText;          // Subtitle and secondary text color
color rcdBodyText;         // Body paragraph text color
color rcdHeadingText;      // Section heading text color
color rcdAccentColor;      // Primary accent color
color rcdLinkColor;        // Hyperlink text color
color rcdHighlightColor;   // Code highlight and gold text color
color rcdTabInactive;      // Inactive tab text color
color rcdTabActive;        // Active tab text color
color rcdTabHover;         // Hovered tab text color
color rcdButtonBg;         // Primary button background color
color rcdButtonBgHover;    // Primary button hover background color
color rcdButtonText;       // Button text color
color rcdCancelBg;         // Cancel button background color
color rcdCancelBgHover;    // Cancel button hover background color
color rcdCheckboxBg;       // Checkbox background color
color rcdCheckboxBorder;   // Checkbox border color
color rcdCheckboxChecked;  // Checkbox checked fill color
color rcdCloseColor;       // Close button icon color
color rcdCloseHoverColor;  // Close button hover icon color
color rcdScrollSlider;     // Scrollbar pill normal color
color rcdScrollSliderHover;// Scrollbar pill hover color
color rcdScrollSliderDrag; // Scrollbar pill drag color

//+------------------------------------------------------------------+
//| Forward declarations                                             |
//+------------------------------------------------------------------+
void        RcdApplyTheme(int theme);
void        RcdCalculateLayout();
bool        RcdCreateCanvases();
void        RcdDestroyCanvases();
bool        RcdLoadLogo();
void        RcdLoadLogoDisplay();
bool        RcdLoadImage(int imgIndex);
void        RcdEnsureImageCache(int imgIndex);
void        RcdBuildContent();
void        RcdRebuildWrappedLines();
void        RcdRenderAll();
void        RcdRenderHeader();
void        RcdRenderTabs();
void        RcdRenderBody();
void        RcdRenderFooter();
void        RcdUpdateHovers(int mouseX, int mouseY);
void        RcdAddPara(RcdPara &paraArray[], RcdParaType paraType, const string paraText = "", int imageIndex = -1);
void        RcdCopyParas(const RcdPara &sourceArray[], RcdPara &destArray[]);
void        RcdGetTabParas(int tabIndex, RcdPara &outputArray[]);
void        RcdWrapText(const RcdPara &paraArray[], int maxPixelWidth, string &outputLines[]);
bool        RcdHasMarkup(const string inputText);
string      RcdStripInlineTags(const string inputText);
void        RcdParseRuns(const string inputText, RcdRun &outputRuns[]);
void        RcdCopyRuns(const RcdRun &sourceRuns[], RcdRun &destRuns[]);
color       RcdResolveColorToken(const string colorToken);
void        RcdStampRuns(CCanvas &targetCanvas, int startX, int startY, int lineH, const RcdRun &runs[], color defaultColor, color stampBg, int fontSize);
void        RcdStampText(CCanvas &targetCanvas, int posX, int posY, const string displayText, const string fontName, int fontSize, color textColor, color bgColor, bool transparentBg);
bool        RcdPointInRect(int pointX, int pointY, int rectX, int rectY, int rectW, int rectH);
int         RcdCalcSliderHeight(int visibleH, int totalH, int trackH, int minH);
void        RcdArgbSplit(uint pixelValue, uchar &alphaOut, uchar &redOut, uchar &greenOut, uchar &blueOut);
uint        RcdBlendPixel(uint destPixel, uint srcPixel);
void        RcdScaleImage(uint &pixelArray[], int origW, int origH, int newW, int newH);
uint        RcdBicubicInterp(uint &pixelArray[], int imgW, int imgH, double sampleX, double sampleY);
double      RcdBicubicComponent(uchar &componentValues[], double fracX, double fracY);
void        RcdFillRoundRectHR(CCanvas &targetCanvas, int rectX, int rectY, int rectW, int rectH, int cornerRadius, uint fillArgb);
void        RcdFillCornerQuadrantHR(CCanvas &targetCanvas, int centerX, int centerY, int radius, uint fillArgb, int signX, int signY);
void        RcdDrawRoundRectBorderHR(CCanvas &targetCanvas, int rectX, int rectY, int rectW, int rectH, int cornerRadius, uint borderArgb, bool drawTop, bool drawLeft, bool drawRight, bool drawBottom, bool arcTL, bool arcTR, bool arcBL, bool arcBR);
void        RcdDownsampleCanvas(CCanvas &targetCanvas, CCanvas &sourceCanvas);
void        RcdImgGetPixels(int imgIndex, uint &outputPixels[]);
void        RcdImgSetPixels(int imgIndex, uint &inputPixels[]);
string      RcdImgResourcePath(int imgIndex);
RcdParaType RcdLineType(const string wrappedLine);
string      RcdLineText(const string wrappedLine);
int         RcdLineIndent(const string wrappedLine);
bool        RcdIsImgLine(const string wrappedLine);
int         RcdImgLineIndex(const string wrappedLine);
int         RcdImgLineSlot(const string wrappedLine);
bool        RcdIsLogoLine(const string wrappedLine);
int         RcdLogoLineSlot(const string wrappedLine);
void        RcdShow();
void        RcdHide();
void        RcdHandleChartEvent(const int eventId, const long &lParam, const double &dParam, const string &sParam);

Мы начинаем реализацию с подключения библиотеки Canvas и встраивания шести растровых файлов непосредственно в скомпилированную программу с помощью директивы #resource — это логотип и пять скриншотов, которые будут отображаться в области содержимого документа. Такое встраивание означает, что они распространяются вместе с самим файлом программы и не требуют никаких внешних ресурсов. Мы объяснили это в первоначальной версии. Затем определяются соответствующие макросы, которые задают для каждого ресурса понятное имя для обращения к нему во всём коде.

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

Далее мы определяем перечисление RcdParaType для классификации всех типов абзацев, которые может содержать документ: пустые разделители, обычный основной текст, заголовки разделов, нумерованные пункты, маркированные пункты, блоки предупреждений, информационные блоки, блоки ответов, заполнители для изображений и заполнители для логотипов. Это определяет, как визуально отображается каждая строка. Наряду с этим структура RcdRun описывает отдельный фрагмент текста с единым оформлением внутри абзаца и содержит сам текст, флаги полужирного начертания, курсива, подчёркивания и зачёркивания, а также переопределённый цвет текста и цвет фонового выделения. Структура  RcdPara представляет собой полный абзац, объединяющий его тип, текстовое содержание и необязательный индекс изображения.

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

Двухпроходный вывод текста на холст с восстановлением альфа-канала

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

//+------------------------------------------------------------------+
//| Stamp text onto canvas using two-pass alpha reconstruction       |
//+------------------------------------------------------------------+
void RcdStampText(CCanvas &targetCanvas, int posX, int posY, const string displayText,
                  const string fontName, int fontSize, color textColor, color bgColor, bool transparentBg)
  {
   if(StringLen(displayText) == 0) return;
   //--- Set the font and measure the text extent
   TextSetFont(fontName, -(fontSize * 10));
   uint textW = 0, textH = 0;
   TextGetSize(displayText, textW, textH);
   if(textW == 0 || textH == 0) return;
   int lineW = (int)textW, lineH = (int)textH;
   //--- Render text onto a black background buffer
   uint bufBlack[];
   ArrayResize(bufBlack, lineW * lineH);
   ArrayFill(bufBlack, 0, lineW * lineH, 0xFF000000);
   TextOut(displayText, 0, 0, TA_LEFT | TA_TOP, bufBlack, lineW, lineH,
           ColorToARGB(textColor, 255), COLOR_FORMAT_ARGB_NORMALIZE);
   //--- Render same text onto a white background buffer
   uint bufWhite[];
   ArrayResize(bufWhite, lineW * lineH);
   ArrayFill(bufWhite, 0, lineW * lineH, 0xFFFFFFFF);
   TextOut(displayText, 0, 0, TA_LEFT | TA_TOP, bufWhite, lineW, lineH,
           ColorToARGB(textColor, 255), COLOR_FORMAT_ARGB_NORMALIZE);
   int canvasW = targetCanvas.Width(), canvasH = targetCanvas.Height();
   //--- Composite each pixel using the difference between buffers to recover true alpha
   for(int py = 0; py < lineH; py++)
     {
      int dstY = posY + py;
      if(dstY < 0 || dstY >= canvasH) continue;
      for(int px = 0; px < lineW; px++)
        {
         int dstX = posX + px;
         if(dstX < 0 || dstX >= canvasW) continue;
         uint pb = bufBlack[py * lineW + px];
         uint pw = bufWhite[py * lineW + px];
         //--- Recover alpha from the channel difference between renders
         int diffR = (int)((pw >> 16) & 0xFF) - (int)((pb >> 16) & 0xFF);
         int diffG = (int)((pw >>  8) & 0xFF) - (int)((pb >>  8) & 0xFF);
         int diffB = (int)( pw        & 0xFF) - (int)( pb        & 0xFF);
         int alpha = 255 - (diffR + diffG + diffB) / 3;
         if(alpha <= 0) continue;
         if(alpha > 255) alpha = 255;
         //--- Reconstruct true RGB from pre-multiplied black-background values
         uchar oR = (alpha > 0) ? (uchar)MathMin(255, (int)((pb >> 16) & 0xFF) * 255 / alpha) : (uchar)((textColor >> 16) & 0xFF);
         uchar oG = (alpha > 0) ? (uchar)MathMin(255, (int)((pb >>  8) & 0xFF) * 255 / alpha) : (uchar)((textColor >>  8) & 0xFF);
         uchar oB = (alpha > 0) ? (uchar)MathMin(255, (int)( pb        & 0xFF) * 255 / alpha) : (uchar)( textColor        & 0xFF);
         //--- Composite reconstructed pixel onto the canvas
         uint stampPixel    = ((uint)(uchar)alpha << 24) | ((uint)oR << 16) | ((uint)oG << 8) | (uint)oB;
         uint existingPixel = targetCanvas.PixelGet(dstX, dstY);
         targetCanvas.PixelSet(dstX, dstY, RcdBlendPixel(existingPixel, stampPixel));
        }
     }
  }

Сначала мы определим функцию RcdStampText, которая будет отвечать за вывод всего текста на поверхность холста. После досрочного выхода в случае пустой строки мы задаём шрифт с помощью функции TextSetFont и измеряем размеры текста с помощью TextGetSize, чтобы определить размеры двух временных пиксельных буферов. Первый буфер заполняется чистым чёрным цветом, а второй — чистым белым, после чего с помощью TextOut в оба буфера выводится один и тот же текст в одной и той же позиции и одним и тем же цветом.

Ключевой вывод носит математический характер: если пиксель глифа полностью непрозрачен, оба буфера отображают один и тот же цвет. Если пиксель полупрозрачен, как бывает на сглаженных краях глифа, результат в чёрном буфере получается темнее, а в белом — светлее. Разница между этими двумя изображениями точно показывает, насколько сильно проступил фон, что напрямую указывает на истинное значение альфа-канала данного пикселя. Для этого мы усредняем разности значений красного, зелёного и синего каналов, а затем вычитаем полученное значение из 255, чтобы получить степень покрытия пикселя глифом.

После восстановления альфа-канала мы восстанавливаем истинные значения RGB, обращая предварительное умножение, возникшее при отрисовке на чёрном фоне: делим значение каждого канала на нормированное значение альфа-канала и ограничиваем результат допустимым диапазоном. Результат упаковывается в пиксель формата ARGB и накладывается на целевой холст с помощью функции RcdBlendPixel, которая использует формулу наложения Портера — Даффа "поверх" для корректного смешивания с уже нарисованным содержимым. Благодаря этому мы получаем четкий текст, не зависящий от фона, в любом месте на любой поверхности. Теперь перейдём к определению изображений, которые мы будем встраивать в документ.

Масштабирование и хранение изображений, управление кэшем

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

//+------------------------------------------------------------------+
//| Scale an image pixel array to new dimensions using bicubic AA    |
//+------------------------------------------------------------------+
void RcdScaleImage(uint &pixelArray[], int origW, int origH, int newW, int newH)
  {
   uint scaledPixels[];
   ArrayResize(scaledPixels, newW * newH);
   //--- Map each destination pixel back to fractional source coordinates
   for(int y = 0; y < newH; y++)
      for(int x = 0; x < newW; x++)
        {
         double ox = (double)x * origW / newW; // Fractional source X
         double oy = (double)y * origH / newH; // Fractional source Y
         scaledPixels[y*newW + x] = RcdBicubicInterp(pixelArray, origW, origH, ox, oy);
        }
   //--- Replace the original pixel array with the scaled result
   ArrayResize(pixelArray, newW * newH);
   ArrayCopy(pixelArray, scaledPixels);
  }

//+------------------------------------------------------------------+
//| Copy pixel data out of an image slot by index                    |
//+------------------------------------------------------------------+
void RcdImgGetPixels(int imgIndex, uint &outputPixels[])
  {
   //--- Copy from the matching flat pixel array based on slot index
   if(imgIndex==0)      ArrayCopy(outputPixels, rcdImgPixels0);
   else if(imgIndex==1) ArrayCopy(outputPixels, rcdImgPixels1);
   else if(imgIndex==2) ArrayCopy(outputPixels, rcdImgPixels2);
   else if(imgIndex==3) ArrayCopy(outputPixels, rcdImgPixels3);
   else if(imgIndex==4) ArrayCopy(outputPixels, rcdImgPixels4);
  }

//+------------------------------------------------------------------+
//| Store pixel data into an image slot by index                     |
//+------------------------------------------------------------------+
void RcdImgSetPixels(int imgIndex, uint &inputPixels[])
  {
   //--- Resize and copy into the matching flat pixel array for the given slot
   if(imgIndex==0)
     { ArrayResize(rcdImgPixels0, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels0, inputPixels); }
   else if(imgIndex==1)
     { ArrayResize(rcdImgPixels1, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels1, inputPixels); }
   else if(imgIndex==2)
     { ArrayResize(rcdImgPixels2, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels2, inputPixels); }
   else if(imgIndex==3)
     { ArrayResize(rcdImgPixels3, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels3, inputPixels); }
   else if(imgIndex==4)
     { ArrayResize(rcdImgPixels4, ArraySize(inputPixels)); ArrayCopy(rcdImgPixels4, inputPixels); }
  }

//+------------------------------------------------------------------+
//| Return the embedded resource path string for an image slot       |
//+------------------------------------------------------------------+
string RcdImgResourcePath(int imgIndex)
  {
   //--- Map slot index to its corresponding #resource path macro
   if(imgIndex==0) return RCD_IMG_MQL5_IDE;
   if(imgIndex==1) return RCD_IMG_MT5_CHART;
   if(imgIndex==2) return RCD_IMG_STRAT_TESTER;
   if(imgIndex==3) return RCD_IMG_MARKET_WATCH;
   if(imgIndex==4) return RCD_IMG_NAVIGATOR;
   return "";
  }

//+------------------------------------------------------------------+
//| Load raw image pixels from embedded resource into a slot         |
//+------------------------------------------------------------------+
bool RcdLoadImage(int imgIndex)
  {
   //--- Validate index bounds
   if(imgIndex < 0 || imgIndex >= RCD_IMG_COUNT) return false;
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read pixels from the embedded resource
   if(!ResourceReadImage(RcdImgResourcePath(imgIndex), px, ow, oh)) return false;
   if(ow == 0 || oh == 0) return false;
   //--- Store original dimensions and pixel data
   rcdImgOrigW[imgIndex] = (int)ow;
   rcdImgOrigH[imgIndex] = (int)oh;
   RcdImgSetPixels(imgIndex, px);
   //--- Mark image as loaded and cache as invalid
   rcdImgLoaded[imgIndex]     = true;
   rcdImgCacheValid[imgIndex] = false;
   return true;
  }

//+------------------------------------------------------------------+
//| Rebuild scaled image cache if panel width has changed            |
//+------------------------------------------------------------------+
void RcdEnsureImageCache(int imgIndex)
  {
   //--- Skip if index invalid or image not yet loaded
   if(imgIndex < 0 || imgIndex >= RCD_IMG_COUNT) return;
   if(!rcdImgLoaded[imgIndex]) return;
   //--- Skip if cache is still valid for the current panel width
   if(rcdImgCacheValid[imgIndex] && rcdImgCacheForWidth[imgIndex] == rcdPanelWidth) return;
   //--- Compute display width fitting inside the text area
   int textAreaW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
   int displayW  = textAreaW - 4;
   //--- Clamp display width between 10 and the original image width
   if(displayW > rcdImgOrigW[imgIndex]) displayW = rcdImgOrigW[imgIndex];
   if(displayW < 10) displayW = 10;
   //--- Maintain aspect ratio for display height
   double aspectRatio = (double)rcdImgOrigH[imgIndex] / (double)rcdImgOrigW[imgIndex];
   int displayH = (int)MathRound(displayW * aspectRatio);
   if(displayH < 1) displayH = 1;
   //--- Re-read original pixels from resource and scale them
   uint px[];
   uint ow = 0, oh = 0;
   if(!ResourceReadImage(RcdImgResourcePath(imgIndex), px, ow, oh)) return;
   RcdScaleImage(px, (int)ow, (int)oh, displayW, displayH);
   //--- Store scaled pixel data and update cache state
   RcdImgSetPixels(imgIndex, px);
   rcdImgScaledW[imgIndex]       = displayW;
   rcdImgScaledH[imgIndex]       = displayH;
   rcdImgCacheValid[imgIndex]    = true;
   rcdImgCacheForWidth[imgIndex] = rcdPanelWidth;
  }

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

Функции RcdImgGetPixels и RcdImgSetPixels обеспечивают доступ по индексу к пяти глобально объявленным одномерным массивам пикселей — по одному для каждого слота изображения. Поскольку MQL5 не поддерживает массивы массивов, для каждого слота используется отдельный массив, а эти две функции скрывают данную особенность реализации, предоставляя доступ по индексу слота. RcdImgResourcePath  завершает реализацию уровня доступа, сопоставляя индекс слота с соответствующей строкой пути к встроенному ресурсу.

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

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

Система встроенной разметки — синтаксический анализ, определение стилей и отрисовка форматированного текста

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

//+------------------------------------------------------------------+
//| Append a paragraph entry to a paragraph array                    |
//+------------------------------------------------------------------+
void RcdAddPara(RcdPara &paraArray[], RcdParaType paraType, const string paraText = "", int imageIndex = -1)
  {
   //--- Extend array by one and populate the new slot
   int currentSize = ArraySize(paraArray);
   ArrayResize(paraArray, currentSize + 1);
   paraArray[currentSize].type   = paraType;
   paraArray[currentSize].text   = paraText;
   paraArray[currentSize].imgIdx = imageIndex;
  }

//+------------------------------------------------------------------+
//| Copy all paragraph entries from source to destination array      |
//+------------------------------------------------------------------+
void RcdCopyParas(const RcdPara &sourceArray[], RcdPara &destArray[])
  {
   int n = ArraySize(sourceArray);
   ArrayResize(destArray, n);
   //--- Copy each field of every paragraph element individually
   for(int i = 0; i < n; i++)
     {
      destArray[i].type   = sourceArray[i].type;
      destArray[i].text   = sourceArray[i].text;
      destArray[i].imgIdx = sourceArray[i].imgIdx;
     }
  }

//+------------------------------------------------------------------+
//| Load the active tab's paragraph array into the output buffer     |
//+------------------------------------------------------------------+
void RcdGetTabParas(int tabIndex, RcdPara &outputArray[])
  {
   //--- Dispatch to the correct per-tab paragraph array
   switch(tabIndex)
     {
      case RCD_TAB_LANGUAGE:   RcdCopyParas(rcdTabParasLanguage,   outputArray); break;
      case RCD_TAB_METAEDITOR: RcdCopyParas(rcdTabParasMetaEditor, outputArray); break;
      case RCD_TAB_PLATFORM:   RcdCopyParas(rcdTabParasPlatform,   outputArray); break;
      case RCD_TAB_TESTER:     RcdCopyParas(rcdTabParasTester,     outputArray); break;
      case RCD_TAB_RESOURCES:  RcdCopyParas(rcdTabParasResources,  outputArray); break;
      default: ArrayResize(outputArray, 0); break;
     }
  }

//+------------------------------------------------------------------+
//| Test whether a text string contains any inline markup tags       |
//+------------------------------------------------------------------+
bool RcdHasMarkup(const string inputText)
  {
   //--- Check for any known opening or closing inline tags
   return (StringFind(inputText, "[b]")  >= 0 || StringFind(inputText, "[u]")  >= 0 ||
           StringFind(inputText, "[i]")  >= 0 || StringFind(inputText, "[s]")  >= 0 ||
           StringFind(inputText, "[/b]") >= 0 || StringFind(inputText, "[/u]") >= 0 ||
           StringFind(inputText, "[/i]") >= 0 || StringFind(inputText, "[/s]") >= 0 ||
           StringFind(inputText, "[/c]") >= 0 || StringFind(inputText, "[/h]") >= 0 ||
           StringFind(inputText, "[c=")  >= 0 || StringFind(inputText, "[h=")  >= 0);
  }

//+------------------------------------------------------------------+
//| Strip all inline markup tags and return plain text               |
//+------------------------------------------------------------------+
string RcdStripInlineTags(const string inputText)
  {
   string result = "";
   int len = StringLen(inputText);
   int i = 0;
   //--- Walk input character by character
   while(i < len)
     {
      if(StringGetCharacter(inputText, i) == '[')
        {
         //--- Find the closing bracket
         int closePos = StringFind(inputText, "]", i);
         //--- Treat unclosed bracket as literal text
         if(closePos < 0) { result += StringSubstr(inputText, i, 1); i++; continue; }
         string tagContent = StringSubstr(inputText, i+1, closePos-i-1);
         //--- Discard recognised markup tags
         if(tagContent == "b"  || tagContent == "/b" || tagContent == "u"  || tagContent == "/u" ||
            tagContent == "i"  || tagContent == "/i" || tagContent == "s"  || tagContent == "/s" ||
            tagContent == "/c" || tagContent == "/h" ||
            StringSubstr(tagContent, 0, 2) == "c=" || StringSubstr(tagContent, 0, 2) == "h=")
           {
            i = closePos + 1;
           }
         else
           {
            //--- Preserve unrecognised bracket sequences as literal text
            result += "[" + tagContent + "]";
            i = closePos + 1;
           }
        }
      else
        {
         //--- Append plain character directly
         result += StringSubstr(inputText, i, 1);
         i++;
        }
     }
   return result;
  }

//+------------------------------------------------------------------+
//| Map a named color token string to its color value                |
//+------------------------------------------------------------------+
color RcdResolveColorToken(const string colorToken)
  {
   //--- Return the theme color matching the token name
   if(colorToken == "accent")  return rcdAccentColor;
   if(colorToken == "heading") return rcdHeadingText;
   if(colorToken == "gold")    return rcdHighlightColor;
   if(colorToken == "red")     return C'220,80,80';
   if(colorToken == "green")   return rcdCheckboxChecked;
   if(colorToken == "dim")     return rcdSubText;
   if(colorToken == "link")    return rcdLinkColor;
   if(colorToken == "warn")    return rcdHighlightColor;
   if(colorToken == "white")   return clrWhite;
   if(colorToken == "black")   return clrBlack;
   //--- Parse hex color literals prefixed with #
   if(StringSubstr(colorToken, 0, 1) == "#")
      return (color)StringToInteger(StringSubstr(colorToken, 1));
   return 0;
  }

//+------------------------------------------------------------------+
//| Copy all run entries from source to destination array            |
//+------------------------------------------------------------------+
void RcdCopyRuns(const RcdRun &sourceRuns[], RcdRun &destRuns[])
  {
   int n = ArraySize(sourceRuns);
   ArrayResize(destRuns, n);
   //--- Copy each field of every run element individually
   for(int i = 0; i < n; i++)
     {
      destRuns[i].text          = sourceRuns[i].text;
      destRuns[i].col           = sourceRuns[i].col;
      destRuns[i].bgCol         = sourceRuns[i].bgCol;
      destRuns[i].bold          = sourceRuns[i].bold;
      destRuns[i].italic        = sourceRuns[i].italic;
      destRuns[i].underline     = sourceRuns[i].underline;
      destRuns[i].strikethrough = sourceRuns[i].strikethrough;
     }
  }

//+------------------------------------------------------------------+
//| Parse inline markup string into a sequence of styled runs        |
//+------------------------------------------------------------------+
void RcdParseRuns(const string inputText, RcdRun &outputRuns[])
  {
   ArrayResize(outputRuns, 0);
   int len = StringLen(inputText);
   if(len == 0) return;
   //--- Track current inline style state
   bool  curBold = false, curItalic = false, curUnderline = false, curStrikethrough = false;
   color curColor = 0, curBgColor = 0;
   string currentSegment = "";
   int i = 0;
   //--- Walk input and emit a run whenever style changes at a tag boundary
   while(i < len)
     {
      if(StringGetCharacter(inputText, i) == '[')
        {
         int closePos = StringFind(inputText, "]", i);
         //--- Treat unclosed bracket as literal character
         if(closePos < 0) { currentSegment += StringSubstr(inputText, i, 1); i++; continue; }
         string tagContent = StringSubstr(inputText, i+1, closePos-i-1);
         //--- Flush accumulated text as a run before applying the tag
         if(StringLen(currentSegment) > 0)
           {
            int sz = ArraySize(outputRuns); ArrayResize(outputRuns, sz+1);
            outputRuns[sz].text          = currentSegment;
            outputRuns[sz].col           = curColor;
            outputRuns[sz].bgCol         = curBgColor;
            outputRuns[sz].bold          = curBold;
            outputRuns[sz].italic        = curItalic;
            outputRuns[sz].underline     = curUnderline;
            outputRuns[sz].strikethrough = curStrikethrough;
            currentSegment = "";
           }
         //--- Apply the recognised tag to the current style state
         if(tagContent == "b")                               curBold = true;
         else if(tagContent == "/b")                         curBold = false;
         else if(tagContent == "i")                          curItalic = true;
         else if(tagContent == "/i")                         curItalic = false;
         else if(tagContent == "u")                          curUnderline = true;
         else if(tagContent == "/u")                         curUnderline = false;
         else if(tagContent == "s")                          curStrikethrough = true;
         else if(tagContent == "/s")                         curStrikethrough = false;
         else if(tagContent == "/c")                         curColor = 0;
         else if(tagContent == "/h")                         curBgColor = 0;
         else if(StringSubstr(tagContent, 0, 2) == "c=")    curColor   = RcdResolveColorToken(StringSubstr(tagContent, 2));
         else if(StringSubstr(tagContent, 0, 2) == "h=")    curBgColor = RcdResolveColorToken(StringSubstr(tagContent, 2));
         else
           {
            //--- Preserve unrecognised bracket sequences verbatim
            currentSegment += "[" + tagContent + "]";
            i = closePos + 1;
            continue;
           }
         i = closePos + 1;
        }
      else
        {
         //--- Accumulate plain character into the current segment
         currentSegment += StringSubstr(inputText, i, 1);
         i++;
        }
     }
   //--- Flush any remaining text as a final run
   if(StringLen(currentSegment) > 0)
     {
      int sz = ArraySize(outputRuns); ArrayResize(outputRuns, sz+1);
      outputRuns[sz].text          = currentSegment;
      outputRuns[sz].col           = curColor;
      outputRuns[sz].bgCol         = curBgColor;
      outputRuns[sz].bold          = curBold;
      outputRuns[sz].italic        = curItalic;
      outputRuns[sz].underline     = curUnderline;
      outputRuns[sz].strikethrough = curStrikethrough;
     }
  }

//+------------------------------------------------------------------+
//| Stamp a sequence of styled runs onto a canvas at a given Y       |
//+------------------------------------------------------------------+
void RcdStampRuns(CCanvas &targetCanvas, int startX, int startY, int lineH,
                  const RcdRun &runs[], color defaultColor, color stampBg, int fontSize)
  {
   int currentX = startX;
   int numRuns  = ArraySize(runs);
   //--- Process each run in sequence
   for(int r = 0; r < numRuns; r++)
     {
      if(StringLen(runs[r].text) == 0) continue;
      //--- Resolve effective text color, falling back to the paragraph default
      color textColor = (runs[r].col != 0) ? runs[r].col : defaultColor;
      //--- Select font variant based on bold/italic flags
      string fontName;
      if(runs[r].bold && runs[r].italic) fontName = "Calibri Bold Italic";
      else if(runs[r].bold)              fontName = "Calibri Bold";
      else if(runs[r].italic)            fontName = "Calibri Italic";
      else                               fontName = "Calibri";
      TextSetFont(fontName, -(fontSize * 10));
      uint runW = 0, runH = 0;
      TextGetSize(runs[r].text, runW, runH);
      int runWidth = (int)runW;
      //--- Detect true glyph top and bottom via a reference render of "H"
      int glyphTop = (int)runH / 4, glyphBot = (int)runH * 3 / 4;
      {
         uint refBuf[];
         int rw = MathMax((int)runW, 8), rh = (int)runH;
         ArrayResize(refBuf, rw * rh);
         ArrayFill(refBuf, 0, rw * rh, 0xFF000000);
         TextOut("H", 0, 0, TA_LEFT | TA_TOP, refBuf, rw, rh, 0xFFFFFFFF, COLOR_FORMAT_ARGB_NORMALIZE);
         //--- Scan downward to find the first row with glyph pixels
         for(int py = 0; py < rh; py++)
           {
            bool found = false;
            for(int px = 0; px < rw; px++)
              {
               if((uchar)((refBuf[py*rw+px] >> 16) & 0xFF) > 60) { glyphTop = py; found = true; break; }
              }
            if(found) break;
           }
         //--- Scan upward to find the last row with glyph pixels
         for(int py = rh-1; py >= 0; py--)
           {
            bool found = false;
            for(int px = 0; px < rw; px++)
              {
               if((uchar)((refBuf[py*rw+px] >> 16) & 0xFF) > 60) { glyphBot = py; found = true; break; }
              }
            if(found) break;
           }
        }
      //--- Draw background highlight rectangle behind the run if requested
      if(runs[r].bgCol != 0 && runWidth > 0)
        {
         int hlTop = startY + glyphTop - 2;
         int hlBot = startY + glyphBot + 4;
         if(hlTop < 0) hlTop = 0;
         if(hlBot >= targetCanvas.Height()) hlBot = targetCanvas.Height() - 1;
         targetCanvas.FillRectangle(currentX, hlTop, currentX + runWidth - 1, hlBot,
                                    ColorToARGB(runs[r].bgCol, 200));
        }
      //--- Determine the background color to pass to the stamp function
      color effectiveBg = (runs[r].bgCol != 0) ? runs[r].bgCol : stampBg;
      //--- Stamp the run text onto the canvas
      RcdStampText(targetCanvas, currentX, startY, runs[r].text, fontName, fontSize, textColor, effectiveBg, true);
      //--- Draw underline decoration below the glyph baseline
      if(runs[r].underline && runWidth > 0)
        {
         int ulY = startY + glyphBot + 2;
         if(ulY < targetCanvas.Height())
            targetCanvas.Line(currentX, ulY, currentX + runWidth - 1, ulY, ColorToARGB(textColor, 255));
        }
      //--- Draw strikethrough decoration through the glyph midpoint
      if(runs[r].strikethrough && runWidth > 0)
        {
         int stY = startY + glyphTop + (glyphBot - glyphTop) * 45 / 100;
         if(stY < targetCanvas.Height())
            targetCanvas.Line(currentX, stY, currentX + runWidth - 1, stY, ColorToARGB(textColor, 255));
        }
      //--- Advance the X cursor by the rendered run width
      currentX += runWidth;
     }
  }

Начнём с уровня управления абзацами. Функция RcdAddPara добавляет один абзац в указанный массив, увеличивая его размер на один элемент и заполняя новую ячейку типом, текстом и (при необходимости) индексом изображения. Функция RcdCopyParas выполняет поэлементное глубокое копирование всех полей массива абзацев, а функция RcdGetTabParas по индексу вкладки выбирает соответствующий ей массив и копирует его в рабочий выходной буфер.

Перед началом любого рендеринга функция RcdHasMarkup с помощью StringFind просматривает строку на наличие известных шаблонов открывающих или закрывающих тегов и возвращает значение true, если такие шаблоны обнаружены. Это служит быстрой проверкой: строки, содержащие обычный текст без разметки, полностью пропускают этапы обработки разметки. Для строк, содержащих разметку, функция RcdStripInlineTags посимвольно просматривает строку, распознаёт и удаляет все известные последовательности тегов, а затем возвращает обычный текст без разметки, используемый для измерения ширины при разбиении текста на строки.

Цветовые токены внутри тегов, таких как [c=accent] или [c=gold], обрабатываются функцией RcdResolveColorToken, которая сопоставляет именованные токены со значениями цветов текущей темы, а также обрабатывает шестнадцатеричные литералы цветов, заданные непосредственно с префиксом #.

Основой системы разметки является RcdParseRuns. Эта функция проходит по входной строке, поддерживая текущее состояние оформления: полужирное и курсивное начертание, подчёркивание, зачёркивание, цвет текста и цвет фона. Каждый раз при обнаружении границы тега накопленный обычный текст без разметки записывается как новый фрагмент текста с единым оформлением, в котором сохраняется текущее состояние оформления, после чего это состояние обновляется в соответствии с тегом. Любые нераспознанные последовательности в квадратных скобках сохраняются без изменений как обычный текст. Весь текст, оставшийся после последнего тега, записывается как завершающий фрагмент текста с единым оформлением.

Наконец, функция RcdStampRuns проходит по массиву фрагментов текста с единым оформлением, полученному при разборе строки, и последовательно отображает каждый фрагмент. Для каждого фрагмента текста с единым оформлением функция определяет фактический цвет текста, выбирает подходящий вариант шрифта с учётом флагов полужирного и курсивного начертания и измеряет ширину фрагмента. Для точного размещения линий подчёркивания и зачёркивания функция отрисовывает эталонный глиф буквы H во временном буфере и сканирует его пиксель за пикселем, чтобы определить фактические верхнюю и нижнюю границы глифа и тем самым избежать неточностей метрик шрифта. Мы выбрали эту букву, поскольку она позволяет определить фактические границы глифа; вы можете использовать любую букву — не обязательно  H. Затем за глифом рисуются заполненные прямоугольники фоновой подсветки, после чего с помощью функции RcdStampText текст выводится на холст. Затем линии подчёркивания и зачёркивания рисуются соответственно на определённой базовой линии глифа и на уровне его середины, а перед переходом к следующему фрагменту горизонтальная позиция курсора увеличивается на ширину отрисованного фрагмента текста с единым оформлением. Далее мы определим вспомогательные функции извлечения данных, которые будут использоваться на протяжении всего процесса разбиения текста на строки.

Служебные функции декодирования строк после разбиения текста

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

//+------------------------------------------------------------------+
//| Test whether a wrapped line is an image placeholder              |
//+------------------------------------------------------------------+
bool RcdIsImgLine(const string wrappedLine)
  {
   //--- Check for the image line prefix at the start of the string
   return StringSubstr(wrappedLine, 0, StringLen(RCD_IMG_LINE_PREFIX)) == RCD_IMG_LINE_PREFIX;
  }

//+------------------------------------------------------------------+
//| Extract the image index encoded in an image placeholder line     |
//+------------------------------------------------------------------+
int RcdImgLineIndex(const string wrappedLine)
  {
   //--- Strip the prefix then parse the integer before the colon separator
   string s = StringSubstr(wrappedLine, StringLen(RCD_IMG_LINE_PREFIX));
   int colon = StringFind(s, ":");
   return (colon > 0) ? (int)StringToInteger(StringSubstr(s, 0, colon)) : 0;
  }

//+------------------------------------------------------------------+
//| Extract the slot number encoded in an image placeholder line     |
//+------------------------------------------------------------------+
int RcdImgLineSlot(const string wrappedLine)
  {
   //--- Strip the prefix then parse the integer after the colon separator
   string s = StringSubstr(wrappedLine, StringLen(RCD_IMG_LINE_PREFIX));
   int colon = StringFind(s, ":");
   return (colon >= 0) ? (int)StringToInteger(StringSubstr(s, colon+1)) : 0;
  }

//+------------------------------------------------------------------+
//| Test whether a wrapped line is a logo placeholder                |
//+------------------------------------------------------------------+
bool RcdIsLogoLine(const string wrappedLine)
  {
   //--- Check for the logo line prefix at the start of the string
   return StringSubstr(wrappedLine, 0, StringLen(RCD_LOGO_LINE_PREFIX)) == RCD_LOGO_LINE_PREFIX;
  }

//+------------------------------------------------------------------+
//| Extract the slot number encoded in a logo placeholder line       |
//+------------------------------------------------------------------+
int RcdLogoLineSlot(const string wrappedLine)
  {
   //--- Parse integer immediately after the logo prefix
   return (int)StringToInteger(StringSubstr(wrappedLine, StringLen(RCD_LOGO_LINE_PREFIX)));
  }

//+------------------------------------------------------------------+
//| Extract the paragraph type encoded at the start of a line        |
//+------------------------------------------------------------------+
RcdParaType RcdLineType(const string wrappedLine)
  {
   //--- Return body type when the type prefix character 'T' is absent
   if(StringSubstr(wrappedLine, 0, 1) != "T") return RCD_PARA_BODY;
   int colon = StringFind(wrappedLine, ":");
   if(colon < 2) return RCD_PARA_BODY;
   //--- Parse the integer type code between 'T' and the first colon
   return (RcdParaType)(int)StringToInteger(StringSubstr(wrappedLine, 1, colon - 1));
  }

//+------------------------------------------------------------------+
//| Extract the display text payload from a wrapped line string     |
//+------------------------------------------------------------------+
string RcdLineText(const string wrappedLine)
  {
   string s = wrappedLine;
   //--- Strip leading type prefix "TN:" if present
   if(StringSubstr(s, 0, 1) == "T")
     {
      int c = StringFind(s, ":");
      if(c >= 1) s = StringSubstr(s, c+1);
     }
   //--- Strip leading indent prefix "INDENT:N:" if present
   if(StringSubstr(s, 0, 7) == "INDENT:")
     {
      int c = StringFind(s, ":", 7);
      if(c > 7) s = StringSubstr(s, c+1);
     }
   return s;
  }

//+------------------------------------------------------------------+
//| Extract the hanging indent pixel width from a wrapped line       |
//+------------------------------------------------------------------+
int RcdLineIndent(const string wrappedLine)
  {
   string s = wrappedLine;
   //--- Strip leading type prefix if present
   if(StringSubstr(s, 0, 1) == "T")
     {
      int c = StringFind(s, ":");
      if(c >= 1) s = StringSubstr(s, c+1);
     }
   //--- Return zero when no indent prefix is present
   if(StringSubstr(s, 0, 7) != "INDENT:") return 0;
   //--- Parse the pixel indent value between "INDENT:" and the following colon
   int c = StringFind(s, ":", 7);
   return (c > 7) ? (int)StringToInteger(StringSubstr(s, 7, c-7)) : 0;
  }

Функции RcdIsImgLine и RcdIsLogoLine проверяют, начинается ли строка, полученная после разбиения текста, с соответствующей константы-префикса, определяя, является ли она заглушкой изображения или логотипа, а не текстовой строкой. Для заглушек изображений функция RcdImgLineIndex удаляет префикс и считывает целое число перед разделителем ":", восстанавливая индекс слота изображения, а функция RcdImgLineSlot считывает целое число после двоеточия, восстанавливая номер вертикального слота: высокое изображение занимает несколько строковых слотов, и только первый из них вызывает фактический вывод пикселей. RcdLogoLineSlot выполняет ту же функцию для заглушек логотипов.

Для текстовых строк функция RcdLineType проверяет, начинается ли строка с символа префикса типа T, и считывает целое число между ним и первым двоеточием, восстанавливая тип абзаца в виде значения RcdParaType; при отсутствии префикса по умолчанию используется тип основного текста. Затем функция RcdLineText удаляет префикс типа и следующий за ним префикс отступа, если он есть, возвращая текст без этих префиксов, готовый к отрисовке. Наконец, функция RcdLineIndent считывает величину отступа в пикселях, сохранённую в префиксе отступа. Она используется для сдвига строк продолжения нумерованных и маркированных абзацев вправо, чтобы они выравнивались по началу текста, а не по маркеру, обеспечивая правильный выступ. Теперь вся логика разбиения текста на строки выглядит следующим образом.

Разбиение абзацев на строки для отображения

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

//+------------------------------------------------------------------+
//| Wrap paragraph array into display lines within pixel width       |
//+------------------------------------------------------------------+
void RcdWrapText(const RcdPara &paraArray[], int maxPixelWidth, string &outputLines[])
  {
   ArrayResize(outputLines, 0);
   int numParas = ArraySize(paraArray);
   //--- Process each paragraph in order
   for(int p = 0; p < numParas; p++)
     {
      //--- Handle image placeholder paragraphs
      if(paraArray[p].type == RCD_PARA_IMG)
        {
         int idx = paraArray[p].imgIdx;
         if(idx >= 0 && idx < RCD_IMG_COUNT && rcdImgLoaded[idx] && rcdLineHeight > 0)
           {
            RcdEnsureImageCache(idx);
            //--- Compute how many line slots the image height occupies
            int totalPx = rcdImgScaledH[idx] + 8;
            int slots = (int)MathCeil((double)totalPx / rcdLineHeight);
            if(slots < 1) slots = 1;
            //--- Emit one placeholder line per slot
            for(int s = 0; s < slots; s++)
              {
               int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
               outputLines[sz] = RCD_IMG_LINE_PREFIX + IntegerToString(idx) + ":" + IntegerToString(s);
              }
           }
         else
           {
            //--- Emit empty line when image is unavailable
            int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
            outputLines[sz] = "";
           }
         continue;
        }
      //--- Handle logo placeholder paragraphs
      if(paraArray[p].type == RCD_PARA_LOGO)
        {
         if(rcdLogoDisplayReady && rcdLineHeight > 0)
           {
            int totalPx = rcdLogoDisplayH + 8;
            int slots = (int)MathCeil((double)totalPx / rcdLineHeight);
            if(slots < 1) slots = 1;
            for(int s = 0; s < slots; s++)
              {
               int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
               outputLines[sz] = RCD_LOGO_LINE_PREFIX + IntegerToString(s);
              }
           }
         else
           {
            int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
            outputLines[sz] = "";
           }
         continue;
        }
      //--- Emit a single blank line for empty paragraphs
      if(paraArray[p].type == RCD_PARA_EMPTY || StringLen(paraArray[p].text) == 0)
        {
         int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
         outputLines[sz] = "";
         continue;
        }

      RcdParaType ptype = paraArray[p].type;
      string ptext      = paraArray[p].text;
      bool isHeading = (ptype == RCD_PARA_HEADING);
      bool isBlock   = (ptype == RCD_PARA_WARN || ptype == RCD_PARA_INFO || ptype == RCD_PARA_ANSWER);
      //--- Select font and size based on paragraph type
      string mFont = isHeading ? "Calibri Bold" : "Calibri";
      int    mSize = isHeading ? RCD_FONT_HEADING : RCD_FONT_BODY;
      TextSetFont(mFont, -(mSize * 10));
      //--- Build the type prefix for stored line strings
      string marker   = "T" + IntegerToString((int)ptype) + ":";
      int    blockPad = isBlock ? 10 : 0;

      //--- Compute hanging indent width for numbered/bullet items
      int hangW = 0;
      if(ptype == RCD_PARA_BULLET)
        {
         uint bW = 0, bH = 0;
         TextGetSize("• ", bW, bH);
         hangW = (int)bW;
        }
      else
        {
         string plainCheck = RcdStripInlineTags(ptext);
         if(StringLen(plainCheck) > 2)
           {
            ushort c0 = StringGetCharacter(plainCheck,0);
            ushort c1 = StringGetCharacter(plainCheck,1);
            ushort c2 = StringGetCharacter(plainCheck,2);
            //--- Detect "X. " numbered list prefix pattern
            if(((c0 >= '1' && c0 <= '9') || (c0 >= 'a' && c0 <= 'z') || (c0 >= 'A' && c0 <= 'Z')) && c1 == '.' && c2 == ' ')
              {
               uint pW = 0, pH = 0;
               TextGetSize(StringSubstr(plainCheck,0,3), pW, pH);
               hangW = (int)pW;
              }
           }
        }

      bool hasMarkup = RcdHasMarkup(ptext);

      if(hasMarkup)
        {
         //--- Markup-aware wrapping: measure plain text widths, preserve original markup in stored lines
         string plain = RcdStripInlineTags(ptext);
         string plainWords[];
         int nw = StringSplit(plain, ' ', plainWords);
         //--- Build origCuts: byte position in ptext where each plain word starts
         int origCuts[];
         ArrayResize(origCuts, nw + 1);
         int pi = 0, li = 0;
         for(int w = 0; w < nw; w++)
           {
            if(w > 0)
              {
               //--- Skip the inter-word space in the plain text
               if(li < StringLen(plain) && StringGetCharacter(plain, li) == ' ') li++;
               //--- Skip any tags and the space between words in the markup source
               bool skippedSpace = false;
               while(pi < StringLen(ptext))
                 {
                  ushort ch = StringGetCharacter(ptext, pi);
                  if(ch == '[')
                    {
                     int cl = StringFind(ptext, "]", pi);
                     if(cl >= 0) { pi = cl+1; continue; }
                    }
                  if(ch == ' ' && !skippedSpace) { pi++; skippedSpace = true; continue; }
                  break;
                 }
               //--- Skip any tags that immediately precede this word
               while(pi < StringLen(ptext) && StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext, "]", pi);
                  if(cl >= 0) pi = cl+1;
                  else break;
                 }
               origCuts[w] = pi;
              }
            else
              {
               origCuts[w] = 0;
               //--- Skip any leading tags before the first word
               while(pi < StringLen(ptext) && StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext, "]", pi);
                  if(cl >= 0) pi = cl+1;
                  else break;
                 }
              }
            //--- Advance both cursors past the word characters
            int wlen = StringLen(plainWords[w]), matched = 0;
            while(pi < StringLen(ptext) && matched < wlen)
              {
               if(StringGetCharacter(ptext, pi) == '[')
                 {
                  int cl = StringFind(ptext,"]",pi);
                  if(cl >= 0) { pi=cl+1; continue; }
                 }
               pi++; li++; matched++;
              }
           }
         origCuts[nw] = StringLen(ptext);

         string stateAtWord[];
         ArrayResize(stateAtWord, nw + 1);
         for(int w2 = 0; w2 <= nw; w2++)
           {
            //--- scanEnd: include all tags whose ] sits at or before this word's start
            int scanEnd = (w2 < nw) ? origCuts[w2] : StringLen(ptext);
            bool stBold=false, stItalic=false, stUnder=false, stStrike=false;
            color stCol=0, stBg=0;
            int spi = 0;
            while(spi < scanEnd)
              {
               if(StringGetCharacter(ptext,spi) == '[')
                 {
                  int cl = StringFind(ptext,"]",spi);
                  //--- Include this tag only if its ] lands at or before scanEnd
                  if(cl >= 0 && cl <= scanEnd)
                    {
                     string tag = StringSubstr(ptext,spi+1,cl-spi-1);
                     if(tag=="b")      stBold=true;
                     else if(tag=="/b") stBold=false;
                     else if(tag=="i")  stItalic=true;
                     else if(tag=="/i") stItalic=false;
                     else if(tag=="u")  stUnder=true;
                     else if(tag=="/u") stUnder=false;
                     else if(tag=="s")  stStrike=true;
                     else if(tag=="/s") stStrike=false;
                     else if(tag=="/c") stCol=0;
                     else if(StringSubstr(tag,0,2)=="c=") stCol=RcdResolveColorToken(StringSubstr(tag,2));
                     else if(tag=="/h") stBg=0;
                     else if(StringSubstr(tag,0,2)=="h=") stBg=RcdResolveColorToken(StringSubstr(tag,2));
                     spi=cl+1;
                     continue;
                    }
                 }
               spi++;
              }
            //--- Serialise active style state as a reopening tag sequence
            string st="";
            if(stBold)   st+="[b]";
            if(stItalic) st+="[i]";
            if(stUnder)  st+="[u]";
            if(stStrike) st+="[s]";
            if(stCol!=0) st+="[c=#"+IntegerToString((int)stCol,6,'0')+"]";
            if(stBg !=0) st+="[h=#"+IntegerToString((int)stBg, 6,'0')+"]";
            stateAtWord[w2] = st;
           }

         //--- Greedy wrap loop: accumulate words into a line until width overflows
         string curPlain      = "";
         int    lineStartW    = 0;
         bool   isFirstLine   = true;
         for(int w = 0; w <= nw; w++)
           {
            bool flush = (w == nw);
            string testPlain = "";
            if(!flush) testPlain = curPlain + (StringLen(curPlain) > 0 ? " " : "") + plainWords[w];
            uint wW = 0, wH = 0;
            if(!flush) TextGetSize(testPlain, wW, wH);
            //--- Subtract block padding and hanging indent from available width
            int effMax = maxPixelWidth - blockPad - 8;
            if(!isFirstLine && hangW > 0) effMax -= hangW;
            //--- Add word to current line if it fits
            if(!flush && (int)wW <= effMax)
              {
               curPlain = testPlain;
              }
            else
              {
               //--- Flush accumulated line to output
               if(StringLen(curPlain) > 0)
                 {
                  int origStart = origCuts[lineStartW];
                  int origEnd   = (w < nw) ? origCuts[w] : StringLen(ptext);
                  //--- Extract the corresponding markup slice from the original text
                  string origSlice = StringSubstr(ptext, origStart, origEnd - origStart);
                  //--- Strip trailing whitespace from the slice
                  while(StringLen(origSlice) > 0 && StringGetCharacter(origSlice, StringLen(origSlice)-1) == ' ')
                     origSlice = StringSubstr(origSlice, 0, StringLen(origSlice)-1);
                  //--- Strip trailing dangling open tags that have no text after them
                  while(StringLen(origSlice) > 0 && StringGetCharacter(origSlice, StringLen(origSlice)-1) == ']')
                    {
                     int openPos = StringLen(origSlice) - 1;
                     while(openPos > 0 && StringGetCharacter(origSlice, openPos) != '[') openPos--;
                     if(openPos >= 0 && StringGetCharacter(origSlice, openPos) == '[')
                       {
                        string trailingTag = StringSubstr(origSlice, openPos+1, StringLen(origSlice)-openPos-2);
                        //--- Remove only opening tags with no following text content
                        bool isOpenTag = (StringLen(trailingTag) > 0 &&
                                          StringGetCharacter(trailingTag, 0) != '/' &&
                                          (StringSubstr(trailingTag,0,2)=="c=" || StringSubstr(trailingTag,0,2)=="h=" ||
                                           trailingTag=="b" || trailingTag=="i" || trailingTag=="u" || trailingTag=="s"));
                        if(isOpenTag) origSlice = StringSubstr(origSlice, 0, openPos);
                        else break;
                       }
                     else break;
                    }
                  //--- Prepend the reopening state tags for continuation lines
                  string prefix = (lineStartW > 0) ? stateAtWord[lineStartW] : "";
                  origSlice = prefix + origSlice;
                  //--- Emit the wrapped line with type marker and optional indent
                  int sz = ArraySize(outputLines); ArrayResize(outputLines, sz+1);
                  if(!isFirstLine && hangW > 0)
                     outputLines[sz] = marker + "INDENT:" + IntegerToString(hangW) + ":" + origSlice;
                  else
                     outputLines[sz] = marker + origSlice;
                  isFirstLine = false;
                 }
               //--- Begin the next line with the current word
               if(!flush) { curPlain = plainWords[w]; lineStartW = w; }
              }
           }
        }
      else
        {
         //--- Plain-text wrapping path: no markup to preserve
         string words[];
         int nw = StringSplit(ptext, ' ', words);
         string cur = "";
         bool isFirstLine = true;
         for(int w = 0; w < nw; w++)
           {
            //--- Build measurement string accounting for any existing indent prefix
            string measureStr;
            if(StringSubstr(cur,0,7)=="INDENT:")
              {
               int c2=StringFind(cur,":",7);
               string tp=(c2>7)?StringSubstr(cur,c2+1):"";
               measureStr=tp+(StringLen(tp)>0?" ":"")+words[w];
              }
            else
              {
               measureStr=cur+(StringLen(cur)>0?" ":"")+words[w];
              }
            uint wW=0,wH=0;
            TextGetSize(measureStr,wW,wH);
            int effMax=maxPixelWidth-blockPad-8;
            if(!isFirstLine&&hangW>0&&StringSubstr(cur,0,7)!="INDENT:") effMax-=hangW;
            //--- Add word to line if it fits within available width
            if((int)wW<=effMax)
              {
               if(StringSubstr(cur,0,7)=="INDENT:")
                 {
                  int c2=StringFind(cur,":",7);
                  string pfx=(c2>7)?StringSubstr(cur,0,c2+1):cur+":";
                  string tp=(c2>7)?StringSubstr(cur,c2+1):"";
                  cur=pfx+tp+(StringLen(tp)>0?" ":"")+words[w];
                 }
               else
                 {
                  cur=cur+(StringLen(cur)>0?" ":"")+words[w];
                 }
              }
            else
              {
               //--- Flush current line and begin a new one
               if(StringLen(cur)>0)
                 {
                  int sz=ArraySize(outputLines);
                  ArrayResize(outputLines,sz+1);
                  outputLines[sz]=marker+cur;
                  isFirstLine=false;
                 }
               //--- Apply hanging indent to continuation lines
               if(!isFirstLine&&hangW>0) cur="INDENT:"+IntegerToString(hangW)+":"+words[w];
               else cur=words[w];
              }
           }
         //--- Flush any remaining text as the final line of this paragraph
         if(StringLen(cur)>0)
           {
            int sz=ArraySize(outputLines);
            ArrayResize(outputLines,sz+1);
            outputLines[sz]=marker+cur;
           }
        }
     }
  }

Функция поочередно обрабатывает каждый абзац. Сначала обрабатываются заглушки изображений и логотипов: их высота в пикселях после масштабирования делится на высоту строки, а результат округляется вверх с помощью функции MathCeil, чтобы определить, сколько строковых слотов они занимают; для каждого слота формируется одна закодированная строка-заглушка. Для каждого пустого абзаца формируется одна пустая строка. Для всех остальных типов абзацев шрифт выбирается в зависимости от того, является ли абзац заголовком или основным текстом. Затем формируется префикс типа, которым помечается каждая выходная строка, чтобы при отрисовке можно было определить тип её абзаца.

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

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

Обработка текста с учётом разметки сложнее. Поскольку ширину необходимо измерять по обычному тексту без разметки, а в сохраняемых строках нужно оставлять исходную разметку, два параллельных курсора одновременно отслеживают позиции в версии с удалёнными тегами и в исходной строке с тегами. Для каждой границы слова в массиве cuts фиксируется смещение в байтах относительно исходного текста разметки. Кроме того, для каждой позиции слова текущее состояние оформления сериализуется в последовательность тегов для повторного открытия. Если в момент разбиения строки был открыт фрагмент с полужирным начертанием или цветовым оформлением, в строке продолжения эти теги автоматически открываются заново, сохраняя оформление при разбиении текста на строки. При записи накопленной строки в выходной массив из исходной строки извлекается соответствующий фрагмент разметки, удаляются пробельные символы и оставшиеся открывающие теги в конце, а в начало добавляется префикс повторного открытия тегов; затем закодированная строка записывается в выходной массив с маркером типа и, при необходимости, префиксом отступа. Теперь мы можем определить содержимое вкладки, которое будем отображать. Это важно, поскольку со встроенной разметкой нужно обращаться внимательно: она должна отображаться непосредственно в содержимом. Мы включили всё это, чтобы вы могли использовать материал как пример при создании собственной документации. Для этого мы использовали материалы о MetaTrader 5 и MQL5.

Создание содержимого документа

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

//+------------------------------------------------------------------+
//| Populate all per-tab paragraph arrays with documentation        |
//+------------------------------------------------------------------+
void RcdBuildContent()
  {
   //--- Assign display names to each tab slot
   rcdTabTitles[RCD_TAB_LANGUAGE]   = "MQL5 Language";
   rcdTabTitles[RCD_TAB_METAEDITOR] = "MetaEditor";
   rcdTabTitles[RCD_TAB_PLATFORM]   = "MT5 Platform";
   rcdTabTitles[RCD_TAB_TESTER]     = "Strategy Tester";
   rcdTabTitles[RCD_TAB_RESOURCES]  = "Resources";

   //--- Convenience macros to reduce repetition inside content blocks
   #define P(t,s)  RcdAddPara(arr,t,s)
   #define PE      RcdAddPara(arr,RCD_PARA_EMPTY)
   #define PI(n)   RcdAddPara(arr,RCD_PARA_IMG,"",n)
   #define PLOGO   RcdAddPara(arr,RCD_PARA_LOGO)
   #define PB(s)   RcdAddPara(arr,RCD_PARA_BULLET,s)

   //========== TAB 1: MQL5 LANGUAGE ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]What Is MQL5?[/u]"); PE;
      P(RCD_PARA_BODY,    "[b]MQL5[/b] — [i]MetaQuotes Language 5[/i] — is the [c=accent]native programming language[/c] of MetaTrader 5. Developed by [b][c=heading]MetaQuotes Software[/c][/b], it gives traders and developers tools to build [c=gold]Expert Advisors[/c], [c=gold]indicators[/c], [c=gold]scripts[/c], and [c=gold]libraries[/c] that run directly inside the MT5 terminal."); PE;
      P(RCD_PARA_BODY,    "Unlike general-purpose languages, MQL5 has [b][c=accent]trading primitives built into the language itself[/c][/b]. Tick data, order execution, position management, and historical price access are [i]not libraries you import[/i] — they are first-class citizens of the language."); PE;

      P(RCD_PARA_HEADING, "The Four Program Types"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] [b]Expert Advisors[/b] — [c=accent]Fully automated trading robots.[/c] Attach to a chart and respond to [b]OnInit[/b], [b]OnTick[/b], [b]OnDeinit[/b], and [b]OnChartEvent[/b]. [i]This entire document — every pixel — is rendered by an EA.[/i]");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] [b]Indicators[/b] — Custom technical analysis drawn on the chart via [c=accent]indicator buffers[/c]. They respond to [b]OnCalculate[/b] and can be called from EAs using [c=link]iCustom()[/c].");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] [b]Scripts[/b] — [i]One-shot programs[/i] that run once and stop. No event loop. Ideal for batch operations and utility tasks.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b][c=teal]Libraries[/c][/b] — Reusable compiled [c=accent].ex5[/c] modules imported via [b]#import[/b]. Cannot run standalone but expose functions to any other MQL5 program."); PE;

      P(RCD_PARA_HEADING, "Core Language Capabilities"); PE;
      PB("[b][c=accent]Object-Oriented:[/c][/b] Full class support — inheritance, polymorphism, encapsulation. The entire standard library is built on OOP.");
      PB("[b][c=gold]Strongly Typed:[/c][/b] Every variable has a declared type. [c=teal]int[/c], [c=teal]double[/c], [c=teal]string[/c], [c=teal]bool[/c], [c=teal]datetime[/c], [c=teal]color[/c], [c=teal]long[/c], [c=teal]ulong[/c], [c=teal]uchar[/c] — each serves a specific purpose.");
      PB("[b][c=purple]Canvas API:[/c][/b] The [c=accent]CCanvas[/c] class provides a pixel-level drawing surface. [i][u]Every heading, every color, every block in this document is CCanvas rendered.[/u][/i]");
      PB("[b][c=orange]Built-in Trading Functions:[/c][/b] [c=link]OrderSend()[/c], [c=link]PositionSelect()[/c], [c=link]HistoryDealSelect()[/c] — trading operations are native language functions."); PE;

      P(RCD_PARA_INFO, "[b]Did you know?[/b] MQL5 compiles to [b][c=green]native machine code[/c][/b] via its own compiler. The resulting [c=accent].ex5[/c] binary runs [b]significantly faster[/b] than interpreted languages — critical for high-frequency strategies processing thousands of ticks per second."); PE;

      P(RCD_PARA_HEADING, "The Event System"); PE;
      P(RCD_PARA_BODY,    "MQL5 programs are [b][i]event-driven[/i][/b]. Your code responds to events the platform fires — [s]not a continuous loop[/s]:"); PE;
      PB("[c=accent]OnInit()[/c] — Fires once on attach. Initialise handles, state, and UI here.");
      PB("[c=accent]OnTick()[/c] — Fires on every new price quote. The [b]heartbeat[/b] of any live EA.");
      PB("[c=accent]OnDeinit()[/c] — Fires on removal or terminal close. [u]Clean up objects and resources.[/u]");
      PB("[c=accent]OnChartEvent()[/c] — Fires on mouse moves, clicks, and keyboard input. [i]All tab switching and scrolling in this document live here.[/i]");
      PB("[c=accent]OnTimer()[/c] — Fires at intervals set by [c=link]EventSetTimer()[/c]. Used for animations and periodic checks.");
      PB("[c=accent]OnTradeTransaction()[/c] — Fires on broker-level trade events. [b][u]The most accurate way[/u][/b] to react to position opens and closes."); PE;

      P(RCD_PARA_HEADING, "Data Types in Depth"); PE;
      P(RCD_PARA_WARN, "[b][c=red]Precision Warning:[/c][/b] Always use [c=accent]double[/c] for prices and call [b]NormalizeDouble(price, _Digits)[/b] before passing values to order functions. [s]Integer arithmetic on prices[/s] causes silent rounding errors that corrupt SL and TP calculations."); PE;
      PB("[b][c=teal]int / long / ulong[/c][/b] — Integer types. [c=accent]ulong[/c] is required for ticket numbers because they can exceed the 32-bit int range on some brokers.");
      PB("[b][c=teal]double[/c][/b] — 64-bit floating point. Used for [i]all[/i] price, volume, and ratio calculations.");
      PB("[b][c=teal]string[/c][/b] — Unicode text. Reference-counted and memory-managed. [c=link]StringLen()[/c], [c=link]StringFind()[/c], [c=link]StringSubstr()[/c] are core tools.");
      PB("[b][c=teal]datetime[/c][/b] — Unix timestamp stored as [c=accent]long[/c] internally. [c=link]TimeCurrent()[/c] returns server time.");
      PB("[b][c=teal]color[/c][/b] — RGB stored as [c=accent]int[/c]. [c=link]ColorToARGB()[/c] adds an alpha channel for canvas rendering. Source format: [h=gold][c=black]C'R,G,B'[/c][/h]."); PE;

      P(RCD_PARA_HEADING, "Arrays and Series"); PE;
      P(RCD_PARA_BODY, "Arrays are declared as [c=accent]type name[][/c] and resized with [c=link]ArrayResize()[/c]. When you set [c=link]ArraySetAsSeries(arr, true)[/c], index [h=accent][c=white]0[/c][/h] is the [b]current bar[/b] and index [h=accent][c=white]1[/c][/h] is the [b]previous completed bar[/b]."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Best Practice:[/c][/b] Always set [c=accent]ArraySetAsSeries(true)[/c] on any price buffer before reading it. [u]Without it, index 0 is the oldest bar[/u] — [b][i][c=red]silently inverting your signal logic.[/c][/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Preprocessor Directives"); PE;
      PB("[b][c=gold]#property[/c][/b] — Sets file metadata: [c=accent]copyright[/c], [c=accent]version[/c], [c=accent]link[/c], [c=accent]description[/c].");
      PB("[b][c=gold]#define[/c][/b] — Compile-time text substitution for named constants that never change.");
      PB("[b][c=gold]#include[/c][/b] — Pastes another file at compile time. Enables modular code using [c=accent].mqh[/c] headers.");
      PB("[b][c=gold]#resource[/c][/b] — [i]Embeds a binary file directly into the compiled[/i] [c=accent].ex5[/c]. [b][u]This document embeds all images this way[/u][/b] — they travel with the file.");
      PB("[b][c=gold]sinput vs input[/c][/b] — [c=accent]sinput[/c] parameters [s]cannot be optimised[/s] in the Strategy Tester. Use it for magic numbers and visual settings."); PE;

      RcdCopyParas(arr, rcdTabParasLanguage);
     }

   //========== TAB 2: METAEDITOR ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MetaEditor — Your MQL5 IDE[/u]"); PE;
      P(RCD_PARA_BODY, "[b]MetaEditor[/b] is the [c=accent]integrated development environment[/c] that ships with every MetaTrader 5 installation. Press [h=gold][c=black]F4[/c][/h] inside MT5 to open it instantly, or navigate to [c=link]Tools → MetaQuotes Language Editor[/c]."); PE;
      PI(RCD_IMG_MQL5IDE_IDX);
      P(RCD_PARA_HEADING, "The Interface Layout"); PE;
      PB("[b][c=gold]Toolbox — Left Panel:[/c][/b] Three tabs — [c=accent]Navigator[/c] for file browsing, [c=accent]Symbols[/c] for instrument lookup, and [c=accent]Projects[/c] for solution management.");
      PB("[b][c=gold]Editor Area — Center:[/c][/b] The main code canvas. [i]Syntax highlighting, code folding, bracket matching, and multi-tab editing.[/i]");
      PB("[b][c=gold]Toolbox — Bottom Panel:[/c][/b] [c=accent]Errors[/c] shows compile errors with exact line numbers. [c=accent]Journal[/c] shows runtime output from [c=link]Print()[/c]."); PE;

      P(RCD_PARA_HEADING, "Writing Your First EA"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] Go to [c=accent]File → New[/c] or press [h=gold][c=black]Ctrl+N[/c][/h]. Select [b]Expert Advisor[/b] and click Next.");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] Enter a name. MetaEditor creates the file with [b]OnInit[/b], [b]OnDeinit[/b], and [b]OnTick[/b] pre-generated in [c=accent]MQL5/Experts/[/c].");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] Write your logic. Add [c=accent]input[/c] parameters at the top for user-configurable settings.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] Press [h=gold][c=black]F7[/c][/h] to compile. [c=green]Warnings[/c] are yellow. [c=red]Errors[/c] are red. Both show exact line numbers.");
      P(RCD_PARA_NUMBERED,"[c=gold]5.[/c] Switch to MT5. Your [c=accent].ex5[/c] appears in the Navigator under Experts. [b][i]Drag it onto any chart to run it.[/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Essential Keyboard Shortcuts"); PE;
      PB("[h=gold][c=black]F7[/c][/h] — Compile. [u]Always compile before testing.[/u]");
      PB("[h=gold][c=black]F5[/c][/h] — Start the debugger and attach to a running EA on a chart.");
      PB("[h=gold][c=black]Ctrl+F[/c][/h] — Find in current file. [c=accent]Ctrl+H[/c] to find and replace.");
      PB("[h=gold][c=black]Ctrl+Shift+F[/c][/h] — Find across [i]all files[/i] in the MQL5 directory.");
      PB("[h=gold][c=black]Ctrl+Space[/c][/h] — IntelliSense autocomplete. Shows function signatures and available methods.");
      PB("[h=gold][c=black]Alt+G[/c][/h] — Go to definition. Jump to where a function or variable is declared."); PE;

      P(RCD_PARA_HEADING, "The Debugger"); PE;
      P(RCD_PARA_BODY, "MetaEditor includes a [b][c=accent]source-level debugger[/c][/b]. Set breakpoints by clicking the line number gutter, press [h=gold][c=black]F5[/c][/h] to attach. Execution pauses — [i]every variable is inspectable live.[/i]"); PE;
      PB("[h=gold][c=black]F10[/c][/h] — Step over. Execute current line and move to next.");
      PB("[h=gold][c=black]F11[/c][/h] — Step into. Enter the body of a function call.");
      PB("[h=gold][c=black]Shift+F11[/c][/h] — Step out. Return from current function to its caller.");
      PB("[c=accent]Locals panel[/c] — See the [b]live value[/b] of every local variable in scope."); PE;

      P(RCD_PARA_INFO, "[b]Performance Matters:[/b] The MetaEditor [c=accent]Profiler[/c] shows [b][u]exactly which functions consume the most CPU[/u][/b] — broken down to individual lines. Use it before any live release to eliminate bottlenecks."); PE;

      P(RCD_PARA_HEADING, "File and Folder Structure"); PE;
      PB("[c=accent]MQL5/Experts/[/c] — All Expert Advisors. Subfolders appear as groups in MT5 Navigator.");
      PB("[c=accent]MQL5/Indicators/[/c] — Custom indicators.");
      PB("[c=accent]MQL5/Scripts/[/c] — One-shot scripts.");
      PB("[c=accent]MQL5/Include/[/c] — Header files [c=teal](.mqh)[/c]. Standard library lives in [c=gold]Include/Trade/[/c] and [c=gold]Include/Canvas/[/c].");
      PB("[c=accent]MQL5/Files/[/c] — Sandbox for file I/O. [c=link]FileOpen()[/c] and [c=link]FileWrite()[/c] are restricted here by default."); PE;

      P(RCD_PARA_WARN, "[b][c=red]Important:[/c][/b] [s]Never edit the .ex5 compiled file directly.[/s] Always edit the [c=accent].mq5[/c] source. The compiled binary is regenerated on every [h=gold][c=black]F7[/c][/h] press. Distributing only the [c=accent].ex5[/c] [b][i]protects your source from copying.[/i][/b]"); PE;

      RcdCopyParas(arr, rcdTabParasMetaEditor);
     }

   //========== TAB 3: MT5 PLATFORM ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MetaTrader 5 — The Platform[/u]"); PE;
      P(RCD_PARA_BODY, "[b]MetaTrader 5[/b] is a [c=accent]multi-asset trading platform[/c] supporting [c=gold]Forex[/c], [c=gold]stocks[/c], [c=gold]futures[/c], [c=gold]options[/c], and [c=gold]CFDs[/c]. It connects to brokers via an encrypted protocol, executes orders, streams live prices, and [b][i]runs your MQL5 programs simultaneously.[/i][/b]"); PE;
      PI(RCD_IMG_MT5CHART_IDX);
      P(RCD_PARA_HEADING, "The Main Interface"); PE;
      PB("[b][c=gold]Menu Bar:[/c][/b] File, View, Insert, Charts, Tools, Window. [c=accent]Tools → Options[/c] configures the platform globally — chart defaults, notifications, server settings.");
      PB("[b][c=red]Algo Trading Button:[/c][/b] The [h=warn][b]smiley face[/b][/h] in the toolbar enables or disables [u]all EA execution globally[/u]. If it is off, [b][c=red]no EA can trade[/c][/b] regardless of its internal settings.");
      PB("[b][c=gold]Market Watch:[/c][/b] The live price feed. [c=accent]Ctrl+M[/c] toggles it. Right-click to add or remove symbols. [i]Drag a symbol onto a chart to switch it instantly.[/i]"); PE;

      P(RCD_PARA_HEADING, "21 Timeframes Available"); PE;
      PB("[c=teal]M1, M2, M3, M4, M5[/c] — Minute timeframes for scalping strategies.");
      PB("[c=teal]M6, M10, M12, M15, M20, M30[/c] — Sub-hourly timeframes.");
      PB("[c=teal]H1, H2, H3, H4, H6, H8, H12[/c] — Hourly timeframes for intraday strategies.");
      PB("[c=teal]D1, W1, MN1[/c] — Daily, weekly, and monthly for [b][i]position and macro trading.[/i][/b]"); PE;

      P(RCD_PARA_HEADING, "Market Watch"); PE;
      PI(RCD_IMG_MWATCH_IDX);
      P(RCD_PARA_BODY, "The [b]Market Watch[/b] window shows live [c=green]Bid[/c] and [c=red]Ask[/c] prices for every symbol. Right-click any row to open a chart, place an order, or view contract specifications."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Tip:[/c][/b] Right-click the [c=accent]column header[/c] to add [b]High[/b], [b]Low[/b], [b][u]Spread[/u][/b], and [b]Volume[/b] columns. The [h=accent][c=white]spread column[/c][/h] is especially useful — it shows live spread in points and reveals [i]true execution costs[/i] before any trade."); PE;

      P(RCD_PARA_HEADING, "The Navigator"); PE;
      PI(RCD_IMG_NAV_IDX);
      P(RCD_PARA_BODY, "Press [h=gold][c=black]Ctrl+N[/c][/h] to open the [b]Navigator[/b]. It shows all MQL5 programs organised by type: [c=gold]Expert Advisors[/c], [c=gold]Indicators[/c], [c=gold]Scripts[/c], and [c=gold]Libraries[/c]."); PE;
      PB("To attach an EA: [c=accent]double-click[/c] it or [b]drag it onto any chart[/b]. The inputs dialog opens automatically.");
      PB("To run a script: drag to chart. [i]Scripts execute immediately[/i] — they cannot be reconfigured after attachment."); PE;

      P(RCD_PARA_HEADING, "Order Types in MT5"); PE;
      PB("[b][c=green]Market Order:[/c][/b] Execute immediately at current price using [c=link]OrderSend()[/c].");
      PB("[b][c=gold]Limit Order:[/c][/b] Execute only at specified price or better.");
      PB("[b][c=orange]Stop Order:[/c][/b] Becomes a market order when price reaches the trigger level.");
      PB("[b][c=purple]Stop-Limit Order:[/c][/b] Becomes a limit order when stop price is hit. [i]Most precise entry control available.[/i]"); PE;

      P(RCD_PARA_WARN, "[b][c=red]Critical — Netting vs Hedging:[/c][/b] Netting accounts allow [u]only one position per symbol[/u]. A second trade in the same direction [s]increases the existing position[/s] instead of opening a new one. [b]Always confirm your account mode[/b] before writing grid or multi-position EAs."); PE;

      RcdCopyParas(arr, rcdTabParasPlatform);
     }

   //========== TAB 4: STRATEGY TESTER ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]The Strategy Tester[/u]"); PE;
      P(RCD_PARA_BODY, "The [b]Strategy Tester[/b] is MT5's [c=accent]built-in backtesting and optimisation engine[/c]. Press [h=gold][c=black]Ctrl+R[/c][/h] to open it. It simulates your EA on historical price data — from [c=dim]open prices only[/c] all the way to [b][c=green]every real tick recorded by the broker[/c][/b]."); PE;
      PI(RCD_IMG_STTEST_IDX);
      P(RCD_PARA_HEADING, "Testing Modes — Fastest to Most Accurate"); PE;
      PB("[b][c=dim]Open Prices Only:[/c][/b] [i]Fastest mode.[/i] Calls [c=accent]OnTick()[/c] only at bar open. [s]Do not use for intrabar strategies.[/s]");
      PB("[b][c=orange]1 Minute OHLC:[/c][/b] Uses M1 bar OHLC to simulate intrabar price movement. Good for most strategies.");
      PB("[b][c=gold]Every Tick (Simulated):[/c][/b] Generates synthetic ticks within each bar using OHLC. Solid accuracy.");
      PB("[b][c=green]Every Tick (Real Ticks):[/c][/b] [b][u]Most accurate.[/u][/b] Uses actual tick data from the broker. [i]Slowest to run — worth it for final validation.[/i]"); PE;

      P(RCD_PARA_HEADING, "Reading the Report"); PE;
      PB("[b][c=gold]Profit Factor[/c][/b] — Gross profit divided by gross loss. Values above [h=accent][c=white]1.5[/c][/h] suggest a robust strategy.");
      PB("[b][c=gold]Max Drawdown[/c][/b] — [b][u]The single most important risk metric.[/u][/b] A strategy with [h=warn][c=black]80% return but 60% drawdown[/c][/h] is [c=red]unusable[/c] for most traders.");
      PB("[b][c=gold]Sharpe Ratio[/c][/b] — Return per unit of risk. Values above [h=accent][c=white]1.0[/c][/h] are acceptable. Above [h=green][c=white]2.0[/c][/h] is excellent.");
      PB("[b][c=gold]Recovery Factor[/c][/b] — Net profit divided by max drawdown. A value of [h=accent][c=white]3.0[/c][/h] or above suggests reliable recovery."); PE;

      P(RCD_PARA_HEADING, "Optimisation"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] Check the [c=accent]optimise checkbox[/c] next to each parameter you want to sweep. Set range and step.");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] Choose your [c=accent]optimisation criterion[/c] — Profit Factor, Expected Payoff, Drawdown, or Sharpe Ratio.");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] Use [c=accent]Genetic Algorithm[/c] for large parameter spaces — finds near-optimal results [b]without testing every combination[/b].");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b][u]Do not simply pick the highest-profit result.[/u][/b] Check robustness across a range of nearby parameter values."); PE;

      P(RCD_PARA_WARN, "[b][c=red]Overfitting Warning:[/c][/b] A strategy optimised to perfection on historical data [s]often fails on live data[/s]. This is [c=accent]curve fitting[/c]. [b]Always validate[/b] on [h=warn][c=black]out-of-sample data[/c][/h] — a period the optimiser never saw."); PE;

      P(RCD_PARA_HEADING, "Forward Testing and Visual Mode"); PE;
      P(RCD_PARA_INFO, "[b]Forward testing[/b] optimises on the [c=accent]in-sample[/c] portion then tests the best result on the [c=accent]out-of-sample[/c] forward period — [b][i]in the same run[/i][/b]. A strategy that performs well on both is a [c=green]strong candidate[/c] for live deployment."); PE;
      P(RCD_PARA_BODY, "Enable [b]Visual Mode[/b] to watch your EA trade bar by bar on an [c=accent]animated chart[/c]. Use it to verify signal logic and drawdown behaviour before running a full statistical backtest."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Rule:[/c][/b] [u]Never go live without a completed backtest.[/u] And [b][c=accent]never go live after just one backtest[/c][/b] — validate across multiple time periods. [i]The market has seen conditions your backtest data has not.[/i]"); PE;

      RcdCopyParas(arr, rcdTabParasTester);
     }

   //========== TAB 5: RESOURCES ==========
   {
      RcdPara arr[];
      P(RCD_PARA_HEADING, "[u]MQL5 Resources and Community[/u]"); PE;
      P(RCD_PARA_BODY, "The [b][c=accent]MQL5 ecosystem[/c][/b] is one of the largest and most active algorithmic trading communities in the world. Whether you are writing your [i]first indicator[/i] or engineering a [b][c=purple]production-grade multi-basket EA[/c][/b], these resources give you everything you need."); PE;
      PLOGO;
      P(RCD_PARA_HEADING, "Official Documentation"); PE;
      PB("[b][c=gold]MQL5 Reference:[/c][/b] [c=link]https://www.mql5.com/en/docs[/c] — The [u]complete language reference[/u]. Every built-in function, enum, and constant documented with examples. [b][i]Bookmark it. You will use it daily.[/i][/b]");
      PB("[b][c=gold]MQL5 Articles:[/c][/b] [c=link]https://www.mql5.com/en/articles[/c] — Thousands of in-depth tutorials covering [c=accent]basic EA structure[/c] to [c=purple]neural networks[/c], [c=teal]genetic algorithms[/c], and [c=orange]market microstructure[/c].");
      PB("[b][c=gold]MT5 Help:[/c][/b] Press [h=gold][c=black]F1[/c][/h] inside MetaTrader 5 or MetaEditor on any function name to jump [b]directly to its documentation page[/b]."); PE;

      P(RCD_PARA_HEADING, "MQL5.community — The Central Hub"); PE;
      PB("[b][c=gold]Market:[/c][/b] Buy and sell EAs, indicators, and scripts. [i]The largest MT4/MT5 product marketplace worldwide.[/i]");
      PB("[b][c=gold]Freelance:[/c][/b] Post a job to hire a developer, or offer your own services. [c=green]Escrow-protected payments.[/c]");
      PB("[b][c=gold]Forum:[/c][/b] Ask questions, share code, report bugs. [c=accent]The MQL5 development team actively participates.[/c]");
      PB("[b][c=gold]Signals:[/c][/b] Subscribe to copy trades from professional providers [b][u]directly into your MT5 account[/u][/b].");
      PB("[b][c=gold]VPS:[/c][/b] Rent a MetaQuotes virtual private server to run your EA [c=green]24/7[/c] [s]without keeping your PC on[/s]."); PE;

      P(RCD_PARA_HEADING, "Standard Library — Your Head Start"); PE;
      P(RCD_PARA_INFO, "[b]Never reinvent the wheel.[/b] The [c=accent]MQL5 Standard Library[/c] ships with every MetaEditor installation as [b][i]pre-built, tested classes[/i][/b]:"); PE;
      PB("[b][c=teal]CTrade[/c][/b] ([c=gold]Trade/Trade.mqh[/c]) — Execute market orders, modifications, and closures. Handles slippage, deviation, and magic numbers cleanly.");
      PB("[b][c=teal]CPositionInfo[/c][/b] ([c=gold]Trade/PositionInfo.mqh[/c]) — Query open position properties [u]without manual PositionSelect() calls[/u].");
      PB("[b][c=teal]CCanvas[/c][/b] ([c=gold]Canvas/Canvas.mqh[/c]) — [b][i]Pixel-level bitmap drawing.[/i][/b] [h=accent][c=white]Everything rendered in this document uses CCanvas.[/c][/h] Text, shapes, images — all pixel-perfect.");
      PB("[b][c=teal]CChartObject[/c][/b] ([c=gold]ChartObjects/*.mqh[/c]) — Object-oriented wrappers for chart lines, arrows, labels, and rectangles."); PE;

      P(RCD_PARA_HEADING, "Your Learning Path"); PE;
      P(RCD_PARA_NUMBERED,"[c=gold]1.[/c] [b]Read the language reference[/b] at [c=link]https://www.mql5.com/en/docs/basis[/c]. [u]Before writing any EA.[/u]");
      P(RCD_PARA_NUMBERED,"[c=gold]2.[/c] [b]Build a simple indicator.[/c] Understanding [c=accent]OnCalculate()[/c] and indicator buffers is the foundation.");
      P(RCD_PARA_NUMBERED,"[c=gold]3.[/c] [b]Write a simple EA.[/c] Start with a [i]Moving Average crossover[/i]. Hard-code first. Then refactor into [c=accent]input[/c] parameters.");
      P(RCD_PARA_NUMBERED,"[c=gold]4.[/c] [b]Add proper trade management.[/c] Implement [c=accent]CTrade[/c], add SL/TP validation, handle spread and stop-level checks.");
      P(RCD_PARA_NUMBERED,"[c=gold]5.[/c] [b][u]Backtest and optimise.[/u][/b] Run on [c=green]real ticks[/c]. Validate out-of-sample. [b][c=red]Never go live without this step.[/c][/b]");
      P(RCD_PARA_NUMBERED,"[c=gold]6.[/c] [b]Demo first — always.[/c] Minimum [h=warn][c=black]30 days on demo[/c][/h]. [i]Market conditions change. Your backtest cannot capture everything.[/i]"); PE;

      P(RCD_PARA_HEADING, "Tip — It Does Not Have to Live Inside Your Main File"); PE;
      P(RCD_PARA_INFO, "[b]Something we recommend:[/b] we do not have to put the entire rich content system inside our main [c=accent].mq5[/c] file. MQL5's [b][c=gold]#include[/c][/b] directive means we can move all of it — the canvas variables, rendering functions, paragraph arrays, and documentation content — into a dedicated [c=teal].mqh[/c] header file. Then our main EA simply has one line at the top: [b][c=accent]#include \"Rich Content Manual.mqh\"[/c][/b]. Everything compiles in. The program works exactly the same. Our main file stays clean."); PE;
      P(RCD_PARA_BODY, "We think of it this way — our EA is built to trade. Our indicator is built to analyse. [i]Neither of them should be carrying hundreds of lines of documentation rendering logic inside them.[/i] A dedicated [c=teal].mqh[/c] file takes all of that out. The main program stays focused on what it does. The manual stays in its own place, does its own job, and plugs in wherever we need it."); PE;
      PB("[b][c=gold]Separation of concerns:[/c][/b] trading logic stays in [c=accent].mq5[/c], documentation logic lives in [c=teal].mqh[/c]. Each file does one thing.");
      PB("[b][c=gold]Reusable:[/c][/b] we update the [c=teal].mqh[/c] once and every program that includes it picks up the change on the next compile. No duplication.");
      PB("[b][c=gold]Easy to ship:[/c][/b] we distribute the [c=teal].mqh[/c] alongside our [c=accent].ex5[/c], or compile everything into one self-contained binary via [b]#resource[/b]. Either way it works.");
      PB("[b][c=gold]Keeps things organised:[/c][/b] as our documentation grows, the [c=teal].mqh[/c] grows with it. Our [c=accent].mq5[/c] does not change — it just includes the file."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Our take:[/c][/b] whether we embed the documentation directly or separate it into its own include file, [u]what the reader experiences is identical[/u] — a rich, scrollable, formatted manual inside MetaTrader 5. [b][i]The architecture is a choice we make as developers. The result speaks for itself.[/i][/b]"); PE;
      P(RCD_PARA_HEADING, "[u]A Word From Us[/u]"); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]\"[/c][/b] [b]We were inspired by a recurring gap[/b] — one we kept seeing between [u]what a program does[/u] and [u]what its user actually understands about it[/u]. Documentation tends to live outside the tool. A separate file. An external link. Something the user has to go and find. [i]We wanted to change that.[/i] We wanted the knowledge to live exactly where the program lives — inside MetaTrader 5, always available, never separated from the tool it describes. [b][c=green]\"[/c][/b]"); PE;
      P(RCD_PARA_INFO, "[b][c=accent]\"[/c][/b] [b]The idea was straightforward:[/b] everything you can do in a [c=accent]PDF[/c] or a [c=accent]Word document[/c] — [b]bold headings[/b], [i]italic emphasis[/i], [u]underlined terms[/u], [c=gold]colored text[/c], [h=warn][c=black]highlighted warnings[/c][/h], numbered lists, bullet points, embedded images, and clearly separated sections — we wanted all of that to be possible inside an MQL5 program, rendered directly on the chart. No external file. No separate reader. [c=teal]The same reading experience a user gets opening a polished document[/c], delivered natively through [b]CCanvas[/b] — scrollable, tabbed, and always attached to the program it describes. [i]Not as an afterthought. As a feature.[/i] [b][c=accent]\"[/c][/b]"); PE;
      P(RCD_PARA_INFO, "[b][c=accent]\"[/c][/b] [b]For the content itself, we deliberately chose [c=teal]MQL5 and MT5[/c] as the subject matter[/b] — the language, the platform, the IDE, the Strategy Tester. Not because it is the only content this system can hold, but because it [i]serves as a fitting description of the very environment this runs in[/i]. If you are building an [b]Expert Advisor[/b], the content becomes your strategy logic, your input explanations, your risk controls. If you are building an [b]indicator[/b], it becomes your signal documentation. A [b]script[/b] — your usage instructions. [u]We focused on the backbone[/u]. The structure, the rendering engine, the markup system, the scroll and tab behavior — [c=accent]all of it is ready to be adapted[/c]. Swap the content, keep the framework, and you have a professional inbuilt manual for any MQL5 program you deliver. [b][c=accent]\"[/c][/b]"); PE;
      P(RCD_PARA_HEADING, "What We Set Out to Enable"); PE;
      P(RCD_PARA_NUMBERED, "[c=gold]a.[/c] [b][c=accent]Self-served documentation.[/c][/b] Users get the full picture the moment they attach a program — no external PDF, no video, no forum thread. Everything they need is already there, inside the chart.");
      P(RCD_PARA_NUMBERED, "[c=gold]b.[/c] [b][c=teal]Documentation that ships with the program.[/c][/b] Compiled directly into the [c=accent].ex5[/c] binary via [b]#resource[/b], it cannot be lost, separated, or left behind. The manual and the tool are always together.");
      P(RCD_PARA_NUMBERED, "[c=gold]c.[/c] [b][c=purple]Formatting that carries meaning.[/c][/b] A [h=warn][c=black]warning[/c][/h] looks different from a [h=accent][c=white]tip[/c][/h]. [c=red]Critical notes[/c] are visually distinct from [c=green]best practices[/c]. [u]The formatting itself communicates[/u] — not just the words.");
      P(RCD_PARA_NUMBERED, "[c=gold]d.[/c] [b][c=orange]A consistent standard for any program type.[/c][/b] Whether it is an [b]Expert Advisor[/b], an [b]indicator[/b], a [b]script[/b], or a [b]library[/b] — the same rich content system works for all of them. [i]Every MQL5 program can now explain itself.[/i]"); PE;
      P(RCD_PARA_WARN, "[b][c=red]\"[/c][/b] [b]We believe developers deserve better tools for explaining their work[/b] — and users deserve better access to that explanation. This document is our contribution toward that. It is open, it is native to MQL5, and [u]anyone can build on it[/u]. Take the approach, adapt it, and make your own programs more transparent to the people who use them. [b][c=red]\"[/c][/b]"); PE;
      P(RCD_PARA_HEADING, "About This Document"); PE;
      P(RCD_PARA_BODY, "This [b][c=accent]Rich Content Document[/c][/b] is itself a [b][i]demonstration[/i][/b] of MQL5's rendering capabilities — [u]scrollable, tabbed, formatted documentation[/u] inside your MT5 chart. Every [h=gold][c=black]highlight[/c][/h], every [c=red]colored word[/c], every [s]strikethrough[/s], every [u]underline[/u], every [b][i][c=purple]bold italic colored combination[/c][/i][/b] you see here is drawn [c=accent]pixel by pixel[/c] using [b]CCanvas[/b], [b]TextOut()[/b], and [b]ResourceCreate()[/b]."); PE;
      P(RCD_PARA_ANSWER, "[b][c=green]Our closing thought:[/c][/b] we built this because we believe [b][c=accent]MQL5 is capable of far more than trading logic[/c][/b]. What you are reading right now is rendered entirely in native MQL5. No external libraries. No outside tools. Just [b]CCanvas[/b], [b]TextOut()[/b], and [b]ResourceCreate()[/b] — and the result is a fully formatted, scrollable document living inside your chart. [i]We hope it changes how you think about what your programs can deliver to the people who use them.[/i] [u]Take it. Adapt it. Make it yours.[/u]"); PE;

      RcdCopyParas(arr, rcdTabParasResources);
     }

   //--- Clean up convenience macros
   #undef P
   #undef PE
   #undef PI
   #undef PLOGO
   #undef PB

   //--- Mark content as built so it is not rebuilt on subsequent shows
   rcdContentBuilt = true;
  }

Функция RcdBuildContent задаёт отображаемые названия для каждой из пяти вкладок, а затем формирует массив абзацев каждой вкладки в отдельном блоке с собственной областью видимости. Чтобы код наполнения документации оставался лаконичным и читаемым, локально определены пять вспомогательных макросов: P для добавления абзаца заданного типа, PE для пустого разделителя, PI для заглушки изображения, PLOGO для заглушки логотипа и PB для пунктов маркированного списка. Все они представляют собой обёртки над вызовами RcdAddPara. После завершения формирования локального массива в блоке каждой вкладки этот массив копируется в соответствующий глобальный массив вкладки с помощью RcdCopyParas, а в конце определения макросов отменяются, чтобы не засорять глобальную область видимости.

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

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

Загрузка и масштабирование логотипа

Логотип отображается в двух местах — в виде небольшого значка на верхней панели и более крупного изображения в области содержимого вкладки "Ресурсы". За каждый вариант отвечает отдельная функция.

//+------------------------------------------------------------------+
//| Load and scale the header logo from the embedded resource        |
//+------------------------------------------------------------------+
bool RcdLoadLogo()
  {
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read raw pixels from the embedded logo resource
   if(!ResourceReadImage(RCD_LOGO_RESOURCE, px, ow, oh)) return false;
   if(ow == 0 || oh == 0) return false;
   //--- Scale to the fixed header logo size
   RcdScaleImage(px, (int)ow, (int)oh, RCD_LOGO_SIZE, RCD_LOGO_SIZE);
   //--- Store as a named in-memory resource for fast header rendering
   return ResourceCreate(rcdLogoScaledResName, px, RCD_LOGO_SIZE, RCD_LOGO_SIZE, 0, 0, RCD_LOGO_SIZE, COLOR_FORMAT_ARGB_NORMALIZE);
  }

//+------------------------------------------------------------------+
//| Load and scale the large body-display logo for Resources tab     |
//+------------------------------------------------------------------+
void RcdLoadLogoDisplay()
  {
   uint px[];
   uint ow = 0, oh = 0;
   //--- Read raw pixels from the embedded logo resource
   if(!ResourceReadImage(RCD_LOGO_RESOURCE, px, ow, oh)) return;
   if(ow == 0 || oh == 0) return;
   int displaySize = 96;
   int dispW, dispH;
   //--- Maintain aspect ratio when scaling to display size
   if((int)ow >= (int)oh)
     {
      dispW = displaySize;
      dispH = (int)MathRound((double)oh / ow * displaySize);
     }
   else
     {
      dispH = displaySize;
      dispW = (int)MathRound((double)ow / oh * displaySize);
     }
   if(dispW < 1) dispW = 1;
   if(dispH < 1) dispH = 1;
   //--- Scale logo pixels to the computed display dimensions
   RcdScaleImage(px, (int)ow, (int)oh, dispW, dispH);
   //--- Store scaled pixel data and mark as ready
   ArrayResize(rcdLogoDisplayPixels, dispW * dispH);
   ArrayCopy(rcdLogoDisplayPixels, px);
   rcdLogoDisplayW     = dispW;
   rcdLogoDisplayH     = dispH;
   rcdLogoDisplayReady = true;
  }

Функция RcdLoadLogo отвечает за вариант логотипа для верхней панели. Она считывает исходные пиксели из встроенного ресурса логотипа с помощью функции ResourceReadImage, а затем масштабирует изображение до фиксированного размера для верхней панели с помощью функции RcdScaleImage. Вместо прямого вывода пикселей при каждой отрисовке верхней панели масштабированное изображение сохраняется как именованный ресурс в памяти с помощью функции ResourceCreate; это позволяет коду отрисовки верхней панели быстро считывать его без повторного масштабирования в каждом кадре. Функция возвращает значение true или false в зависимости от того, удалось ли успешно создать ресурс.

Функция RcdLoadLogoDisplay отвечает за более крупный вариант логотипа, отображаемый в области содержимого вкладки "Ресурсы". Она считывает тот же встроенный логотип, но масштабирует его до 96 пикселей по большей стороне, сохраняя исходное соотношение сторон: если ширина изображения больше высоты, ширина задаётся равной 96 пикселям, а высота рассчитывается пропорционально, и наоборот. При этом каждый размер должен составлять не менее 1 пикселя, чтобы избежать ошибок, связанных с нулевыми размерами. Масштабированные пиксели копируются в глобальный массив пикселей отображаемого изображения; также сохраняются размеры отображаемого изображения и флаг готовности, чтобы код отрисовки области содержимого мог определить, что логотип доступен и сколько именно места он занимает при размещении строк. Далее мы создаем макет.

Компоновка панели, создание и уничтожение холстов

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

//+------------------------------------------------------------------+
//| Compute and cache panel geometry from current chart dimensions   |
//+------------------------------------------------------------------+
void RcdCalculateLayout()
  {
   //--- Read chart pixel dimensions with safe fallbacks
   long chartW = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   long chartH = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   if(chartW < 200) chartW = 800;
   if(chartH < 200) chartH = 600;
   //--- Compute panel width clamped between min and max values
   int targetW = (int)(chartW - RCD_SIDE_MARGIN * 2);
   rcdPanelWidth = MathMin(RCD_MAX_WIDTH, MathMax(RCD_MIN_WIDTH, targetW));
   if(rcdPanelWidth > (int)chartW - 20)
      rcdPanelWidth = MathMax(RCD_MIN_WIDTH, (int)chartW - 20);
   //--- Compute body height from available vertical space minus chrome
   int chromeH    = RCD_HEADER_H + RCD_GAP + RCD_TABS_H + RCD_GAP + RCD_GAP + RCD_FOOTER_H;
   int availBodyH = (int)(chartH - RCD_TOP_MARGIN - RCD_BOTTOM_MARGIN - chromeH);
   rcdBodyHeight  = MathMin(RCD_MAX_BODY_H, MathMax(RCD_MIN_BODY_H, availBodyH));
   //--- Compute total panel height as the sum of all sections
   rcdTotalHeight = RCD_HEADER_H + RCD_GAP + RCD_TABS_H + RCD_GAP + rcdBodyHeight + RCD_GAP + RCD_FOOTER_H;
   //--- Centre panel horizontally within the chart
   rcdPanelX = MathMax(RCD_SIDE_MARGIN, (int)((chartW - rcdPanelWidth) / 2));
   rcdPanelY = RCD_TOP_MARGIN;
   //--- Clamp panel Y so it does not overflow the bottom margin
   if(rcdPanelY + rcdTotalHeight > (int)chartH - RCD_BOTTOM_MARGIN)
      rcdPanelY = MathMax(RCD_TOP_MARGIN, (int)chartH - rcdTotalHeight - RCD_BOTTOM_MARGIN);
   //--- Compute absolute Y coordinates for each panel section
   rcdHeaderY = rcdPanelY;
   rcdTabsY   = rcdHeaderY + RCD_HEADER_H + RCD_GAP;
   rcdBodyY   = rcdTabsY   + RCD_TABS_H   + RCD_GAP;
   rcdFooterY = rcdBodyY   + rcdBodyHeight + RCD_GAP;
   //--- Invalidate image caches when the panel width has changed
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
      if(rcdImgCacheForWidth[ii] != rcdPanelWidth)
         rcdImgCacheValid[ii] = false;
  }

//+------------------------------------------------------------------+
//| Create all six canvas bitmap label objects                       |
//+------------------------------------------------------------------+
bool RcdCreateCanvases()
  {
   //--- Create header canvas at the computed position and size
   if(!rcdCanvHeader.CreateBitmapLabel(0, 0, rcdHeaderCanvasName,  rcdPanelX, rcdHeaderY, rcdPanelWidth, RCD_HEADER_H,  COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create tabs canvas directly below the header
   if(!rcdCanvTabs.CreateBitmapLabel(0, 0, rcdTabsCanvasName,      rcdPanelX, rcdTabsY,   rcdPanelWidth, RCD_TABS_H,    COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create body canvas for background fill
   if(!rcdCanvBody.CreateBitmapLabel(0, 0, rcdBodyCanvasName,      rcdPanelX, rcdBodyY,   rcdPanelWidth, rcdBodyHeight, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create internal high-resolution body canvas for supersampling
   if(!rcdCanvBodyHR.Create(rcdBodyHRCanvasName, rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create block overlay canvas for text, images, and scrollbar
   if(!rcdCanvBlock.CreateBitmapLabel(0, 0, rcdBlockCanvasName,    rcdPanelX, rcdBodyY,   rcdPanelWidth, rcdBodyHeight, COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   //--- Create footer canvas at the bottom of the panel
   if(!rcdCanvFooter.CreateBitmapLabel(0, 0, rcdFooterCanvasName,  rcdPanelX, rcdFooterY, rcdPanelWidth, RCD_FOOTER_H,  COLOR_FORMAT_ARGB_NORMALIZE)) return false;
   return true;
  }

//+------------------------------------------------------------------+
//| Destroy all canvases and remove their chart objects              |
//+------------------------------------------------------------------+
void RcdDestroyCanvases()
  {
   //--- Destroy each canvas and delete its associated chart object
   rcdCanvHeader.Destroy(); ObjectDelete(0, rcdHeaderCanvasName);
   rcdCanvTabs.Destroy();   ObjectDelete(0, rcdTabsCanvasName);
   rcdCanvBody.Destroy();   ObjectDelete(0, rcdBodyCanvasName);
   rcdCanvBodyHR.Destroy();
   rcdCanvBlock.Destroy();  ObjectDelete(0, rcdBlockCanvasName);
   rcdCanvFooter.Destroy(); ObjectDelete(0, rcdFooterCanvasName);
  }

Функция RcdCalculateLayout считывает текущую ширину и высоту графика с помощью функции ChartGetInteger, применяя безопасные резервные значения для необычно маленьких графиков. Ширина панели рассчитывается путём вычитания боковых полей из ширины графика, после чего ограничивается диапазоном между заданными константами минимальной и максимальной ширины; дополнительная проверка гарантирует, что она не превышает ширину графика за вычетом небольшого защитного отступа. Высота области содержимого рассчитывается по оставшемуся вертикальному пространству после вычитания верхнего и нижнего полей, а также суммарной высоты верхней панели, панели вкладок и нижней панели; полученное значение также ограничивается минимальной и максимальной границами. Таким образом, общая высота панели равна сумме высот всех секций. Панель центрируется по горизонтали и размещается по вертикали с учётом верхнего поля; положение её нижнего края проверяется, чтобы панель не заходила на нижнее поле. Исходя из окончательного положения панели, последовательно вычисляются абсолютные вертикальные координаты верхней панели, вкладок, области содержимого и нижней панели. Наконец, если ширина панели изменилась с момента последнего формирования кэшей изображений, все эти кэши помечаются как недействительные, что вызывает повторное масштабирование при следующем проходе отрисовки.

Функция RcdCreateCanvases создаёт все шесть поверхностей рисования в координатах, рассчитанных функцией компоновки. Холсты верхней панели, вкладок, фона области содержимого, слоя блоков и нижней панели создаются как объекты графика типа "растровая метка" с помощью CreateBitmapLabel, благодаря чему они отображаются на графике в заданных координатах. Внутренний холст области содержимого с высоким разрешением создаётся иначе — с помощью Create без создания объекта графика, поскольку он служит исключительно внеэкранным буфером суперсэмплинга и никогда не отображается напрямую. Если создание какого-либо холста завершилось с ошибкой, функция немедленно возвращает значение false.

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

Повторное формирование массива строк после разбиения текста

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

//+------------------------------------------------------------------+
//| Recompute wrapped line array for the active tab content          |
//+------------------------------------------------------------------+
void RcdRebuildWrappedLines()
  {
   //--- Compute available text width excluding scrollbar and padding
   int textAreaW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
   //--- Measure line height from a single reference character
   TextSetFont("Calibri", -(RCD_FONT_BODY * 10));
   uint tw = 0, th = 0;
   TextGetSize("x", tw, th);
   rcdLineHeight = (int)(th * 0.65) + RCD_LINE_GAP;
   //--- Ensure all loaded image caches are current for this panel width
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
      if(rcdImgLoaded[ii]) RcdEnsureImageCache(ii);
   //--- Load the active tab's paragraph array and wrap it into display lines
   RcdGetTabParas(rcdActiveTab, rcdCurrentParas);
   RcdWrapText(rcdCurrentParas, textAreaW, rcdWrappedLines);
   //--- Compute total content height and scroll bounds
   int numLines          = ArraySize(rcdWrappedLines);
   rcdTotalContentHeight = numLines * rcdLineHeight + RCD_TOP_PAD_BODY * 2;
   rcdScrollVisible      = rcdTotalContentHeight > rcdBodyHeight;
   rcdMaxScroll          = MathMax(0, rcdTotalContentHeight - rcdBodyHeight);
   rcdScrollPos          = MathMax(0, MathMin(rcdScrollPos, rcdMaxScroll));
   //--- Compute scrollbar pill height when scroll is visible
   if(rcdScrollVisible)
     {
      int pillMargin  = 4;
      int trackH      = rcdBodyHeight - pillMargin * 2;
      rcdSliderHeight = RcdCalcSliderHeight(rcdBodyHeight, rcdTotalContentHeight, trackH, 24);
     }
  }

Функция RcdRebuildWrappedLines начинает работу с вычисления доступной ширины текста: из общей ширины панели вычитаются левый и правый внутренние отступы, а также ширина скруглённого ползунка полосы прокрутки и отступы вокруг него. Затем высота строки определяется по эталонному символу с помощью TextGetSize при размере шрифта области содержимого: результат уменьшается до 65 %, чтобы убрать избыточные внутренние вертикальные отступы шрифта, после чего к нему прибавляется заданная константа межстрочного зазора. В результате получается более плотный и визуально более единообразный межстрочный интервал, чем тот, который давали бы исходные метрики шрифта.

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

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

//+------------------------------------------------------------------+
//| Render body background, block highlights, text, and scrollbar    |
//+------------------------------------------------------------------+
void RcdRenderBody()
  {
   //--- Fill the HR canvas with the background color
   rcdCanvBodyHR.Erase(0);
   uint bgArgb = ColorToARGB(rcdBg, 255);
   rcdCanvBodyHR.FillRectangle(0, 0, rcdPanelWidth*RCD_SS-1, rcdBodyHeight*RCD_SS-1, bgArgb);
   //--- Downsample to the display body canvas and add border lines
   RcdDownsampleCanvas(rcdCanvBody, rcdCanvBodyHR);
   uint borderArgb = ColorToARGB(rcdBorder, 255);
   rcdCanvBody.Line(0, 0, 0, rcdBodyHeight-1, borderArgb);
   rcdCanvBody.Line(rcdPanelWidth-1, 0, rcdPanelWidth-1, rcdBodyHeight-1, borderArgb);
   rcdCanvBody.Update();
   //--- Clear the block overlay canvas
   rcdCanvBlock.Erase(0x00000000);
   //--- Detect dark theme for block color selection
   bool isDark = (rcdBg == C'20,24,34');

   //--- Draw colored background panels behind WARN / INFO / ANSWER blocks
   {
      int numLines = ArraySize(rcdWrappedLines);
      int topPad   = RCD_TOP_PAD_BODY;
      int blockX   = RCD_PAD - 3;
      int blockW   = rcdPanelWidth - blockX - RCD_SB_PILL_W - RCD_SB_MARGIN_R - 2;
      for(int i = 0; i < numLines; )
        {
         string ln = rcdWrappedLines[i];
         if(RcdIsImgLine(ln) || RcdIsLogoLine(ln)) { i++; continue; }
         if(StringLen(ln) == 0) { i++; continue; }
         RcdParaType lineType = RcdLineType(ln);
         bool isWarn   = (lineType == RCD_PARA_WARN);
         bool isInfo   = (lineType == RCD_PARA_INFO);
         bool isAnswer = (lineType == RCD_PARA_ANSWER);
         if(!isWarn && !isInfo && !isAnswer) { i++; continue; }
         //--- Find the last line that belongs to this block run
         int blockStart = i, blockEnd = i;
         for(int j = i+1; j < numLines; j++)
           {
            if(RcdIsImgLine(rcdWrappedLines[j]) || RcdIsLogoLine(rcdWrappedLines[j])) break;
            if(StringLen(rcdWrappedLines[j]) == 0)
              {
               //--- Look ahead to see if the block continues after a blank line
               bool cont = false;
               for(int k = j+1; k < numLines && k <= j+2; k++)
                 {
                  if(RcdIsImgLine(rcdWrappedLines[k]) || RcdIsLogoLine(rcdWrappedLines[k])) break;
                  RcdParaType tk = RcdLineType(rcdWrappedLines[k]);
                  if((isWarn&&tk==RCD_PARA_WARN)||(isInfo&&tk==RCD_PARA_INFO)||(isAnswer&&tk==RCD_PARA_ANSWER)) { cont=true; break; }
                  if(StringLen(rcdWrappedLines[k]) > 0) break;
                 }
               if(cont) blockEnd = j; else break;
              }
            else
              {
               RcdParaType tj = RcdLineType(rcdWrappedLines[j]);
               if((isWarn&&tj==RCD_PARA_WARN)||(isInfo&&tj==RCD_PARA_INFO)||(isAnswer&&tj==RCD_PARA_ANSWER))
                  blockEnd = j;
               else break;
              }
           }
         //--- Compute visible top/bottom pixel coordinates for this block
         int dtop = MathMax(0, topPad + blockStart * rcdLineHeight - rcdScrollPos - 3);
         int dbot = MathMin(rcdBodyHeight-1, topPad + (blockEnd+1) * rcdLineHeight - rcdScrollPos + 3);
         if(dbot > dtop)
           {
            //--- Choose fill and accent bar colors by block type and theme
            color bgC  = isWarn   ? (isDark ? C'75,18,18'  : C'255,210,210') :
                         isAnswer ? (isDark ? C'14,52,18'  : C'200,240,210') :
                                    (isDark ? C'16,33,68'  : C'210,225,250');
            color barC = isWarn   ? (isDark ? C'195,45,45' : C'175,28,28')   :
                         isAnswer ? (isDark ? C'48,172,75' : C'28,136,58')   :
                                    (isDark ? C'52,120,240': C'28,88,200');
            //--- Fill block background and the left-side accent bar
            rcdCanvBlock.FillRectangle(blockX, dtop, blockX+blockW-1, dbot, ColorToARGB(bgC, 255));
            rcdCanvBlock.FillRectangle(blockX, dtop, blockX+2,        dbot, ColorToARGB(barC, 255));
           }
         i = blockEnd + 1;
        }
     }

   //--- Draw all text lines and inline images onto the block canvas
   int textX    = RCD_PAD;
   int numLines = ArraySize(rcdWrappedLines);
   int topPad   = RCD_TOP_PAD_BODY;
   //--- Track which images have already been drawn this pass
   bool imgDrawnFlags[RCD_IMG_COUNT];
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++) imgDrawnFlags[ii] = false;
   for(int i = 0; i < numLines; i++)
     {
      string ln    = rcdWrappedLines[i];
      int    lineY = topPad + i * rcdLineHeight - rcdScrollPos;
      //--- Handle image placeholder lines
      if(RcdIsImgLine(ln))
        {
         int imgIdx = RcdImgLineIndex(ln);
         int slot   = RcdImgLineSlot(ln);
         //--- Draw image only once, on slot 0, when cache is valid
         if(slot == 0 && imgIdx >= 0 && imgIdx < RCD_IMG_COUNT &&
            rcdImgLoaded[imgIdx] && rcdImgCacheValid[imgIdx] && !imgDrawnFlags[imgIdx])
           {
            imgDrawnFlags[imgIdx] = true;
            int imgY  = lineY + 4;
            int textW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
            //--- Centre image horizontally within the text area
            int imgX  = RCD_PAD + (textW - rcdImgScaledW[imgIdx]) / 2;
            if(imgX < 0) imgX = 0;
            uint px[];
            RcdImgGetPixels(imgIdx, px);
            //--- Blit each pixel of the scaled image onto the block canvas
            for(int py = 0; py < rcdImgScaledH[imgIdx]; py++)
              {
               int dstY = imgY + py;
               if(dstY < 0) continue;
               if(dstY >= rcdBodyHeight) break;
               for(int px2 = 0; px2 < rcdImgScaledW[imgIdx]; px2++)
                 {
                  int dstX = imgX + px2;
                  if(dstX < 0 || dstX >= rcdPanelWidth) continue;
                  uint srcPx = px[py * rcdImgScaledW[imgIdx] + px2];
                  uchar sa, sr, sg, sb;
                  RcdArgbSplit(srcPx, sa, sr, sg, sb);
                  if(sa == 0) continue;
                  uint ex = rcdCanvBlock.PixelGet(dstX, dstY);
                  rcdCanvBlock.PixelSet(dstX, dstY, RcdBlendPixel(ex, srcPx));
                 }
              }
           }
         continue;
        }
      //--- Handle logo placeholder lines
      if(RcdIsLogoLine(ln))
        {
         int slot = RcdLogoLineSlot(ln);
         //--- Draw logo only once, on slot 0, when display data is ready
         if(slot == 0 && rcdLogoDisplayReady)
           {
            int imgY  = lineY + 4;
            int textW = rcdPanelWidth - RCD_PAD * 2 - RCD_SB_PILL_W - RCD_SB_MARGIN_R * 2;
            int imgX  = RCD_PAD + (textW - rcdLogoDisplayW) / 2;
            if(imgX < 0) imgX = 0;
            for(int py = 0; py < rcdLogoDisplayH; py++)
              {
               int dstY = imgY + py;
               if(dstY < 0) continue;
               if(dstY >= rcdBodyHeight) break;
               for(int px2 = 0; px2 < rcdLogoDisplayW; px2++)
                 {
                  int dstX = imgX + px2;
                  if(dstX < 0 || dstX >= rcdPanelWidth) continue;
                  uint srcPx = rcdLogoDisplayPixels[py * rcdLogoDisplayW + px2];
                  uchar sa, sr, sg, sb;
                  RcdArgbSplit(srcPx, sa, sr, sg, sb);
                  if(sa == 0) continue;
                  uint ex = rcdCanvBlock.PixelGet(dstX, dstY);
                  rcdCanvBlock.PixelSet(dstX, dstY, RcdBlendPixel(ex, srcPx));
                 }
              }
           }
         continue;
        }
      //--- Skip lines scrolled above or below the visible area
      if(lineY + rcdLineHeight < 0) continue;
      if(lineY >= rcdBodyHeight) break;
      if(StringLen(ln) == 0) continue;
      //--- Compute horizontal text position including hanging indent
      int lineTextX = textX;
      int indentPx  = RcdLineIndent(ln);
      if(indentPx > 0) lineTextX = textX + indentPx;
      //--- Decode paragraph type and display text from wrapped line
      RcdParaType ptype   = RcdLineType(ln);
      string renderText   = RcdLineText(ln);
      bool isBlock        = (ptype == RCD_PARA_WARN || ptype == RCD_PARA_INFO || ptype == RCD_PARA_ANSWER);
      //--- Add extra indent inside block paragraphs for the accent bar
      if(isBlock) lineTextX = textX + 10 + indentPx;
      //--- Measure bullet prefix width and shift text right for first line
      if(ptype == RCD_PARA_BULLET && indentPx == 0)
        {
         TextSetFont("Calibri", -(RCD_FONT_BODY * 10));
         uint bW = 0, bH = 0;
         TextGetSize("• ", bW, bH);
         lineTextX = textX + (int)bW;
        }
      //--- Select the default text color for this paragraph type
      color defaultTextColor;
      switch(ptype)
        {
         case RCD_PARA_HEADING:  defaultTextColor = rcdHeadingText;    break;
         case RCD_PARA_NUMBERED: defaultTextColor = rcdHighlightColor; break;
         default:                defaultTextColor = rcdBodyText;       break;
        }
      //--- Select font face and size based on paragraph type
      string fontName = (ptype == RCD_PARA_HEADING) ? "Calibri Bold" : "Calibri";
      int    fontSize = (ptype == RCD_PARA_HEADING) ? RCD_FONT_HEADING : RCD_FONT_BODY;
      //--- Determine the effective background color for text stamping
      color stampBg;
      if(isBlock)
         stampBg = (ptype == RCD_PARA_WARN)   ? (isDark ? C'75,18,18'  : C'255,210,210') :
                   (ptype == RCD_PARA_ANSWER)  ? (isDark ? C'14,52,18'  : C'200,240,210') :
                                                 (isDark ? C'16,33,68'  : C'210,225,250');
      else
         stampBg = rcdBg;
      //--- Stamp line content using markup-aware or plain path
      if(RcdHasMarkup(renderText))
        {
         RcdRun runs[];
         RcdParseRuns(renderText, runs);
         //--- Stamp the bullet character separately before the run text
         if(ptype == RCD_PARA_BULLET && indentPx == 0)
            RcdStampText(rcdCanvBlock, textX, lineY, "•", "Calibri", fontSize, defaultTextColor, stampBg, true);
         RcdStampRuns(rcdCanvBlock, lineTextX, lineY, rcdLineHeight, runs, defaultTextColor, stampBg, fontSize);
        }
      else
        {
         //--- Stamp bullet character then plain text
         if(ptype == RCD_PARA_BULLET && indentPx == 0)
            RcdStampText(rcdCanvBlock, textX, lineY, "•", "Calibri", fontSize, defaultTextColor, stampBg, true);
         RcdStampText(rcdCanvBlock, lineTextX, lineY, renderText, fontName, fontSize, defaultTextColor, stampBg, true);
        }
     }

   //--- Draw the scrollbar pill when scroll is visible and mouse is nearby
   if(rcdScrollVisible && (rcdMouseInBody || rcdIsDraggingSlider))
     {
      int pillMargin = 4;
      int trackH     = rcdBodyHeight - pillMargin * 2;
      int thumbY     = pillMargin;
      //--- Compute the pill's vertical position proportional to scroll offset
      if(rcdMaxScroll > 0)
         thumbY = pillMargin + (int)(((double)rcdScrollPos / rcdMaxScroll) * (trackH - rcdSliderHeight));
      thumbY = MathMax(pillMargin, MathMin(rcdBodyHeight - rcdSliderHeight - pillMargin, thumbY));
      int pillX = rcdPanelWidth - RCD_SB_MARGIN_R - RCD_SB_PILL_W;
      //--- Choose pill color based on drag/hover state
      color pillColor;
      uchar pillAlpha;
      if(rcdIsDraggingSlider)  { pillColor = rcdScrollSliderDrag;  pillAlpha = 255; }
      else if(rcdHoverSlider)  { pillColor = rcdScrollSliderHover; pillAlpha = 255; }
      else                     { pillColor = rcdScrollSlider;       pillAlpha = 180; }
      uint thumbArgb = ColorToARGB(pillColor, pillAlpha);
      //--- Build and downsample an HR pill onto the block canvas
      int pWS = RCD_SB_PILL_W * RCD_SS, pHS = rcdSliderHeight * RCD_SS;
      CCanvas pillHR;
      pillHR.Create("RCD_PillHR_scroll_tmp", pWS, pHS, COLOR_FORMAT_ARGB_NORMALIZE);
      pillHR.Erase(0x00000000);
      RcdFillRoundRectHR(pillHR, 0, 0, pWS, pHS, MathMax(1, pWS/2), thumbArgb);
      int ss2 = RCD_SS * RCD_SS;
      for(int py = 0; py < rcdSliderHeight; py++)
         for(int px2 = 0; px2 < RCD_SB_PILL_W; px2++)
           {
            double sumA=0,sumR=0,sumG=0,sumB=0,wc=0;
            for(int dy=0;dy<RCD_SS;dy++)
               for(int dx=0;dx<RCD_SS;dx++)
                 {
                  int sx=px2*RCD_SS+dx, sy=py*RCD_SS+dy;
                  if(sx>=pWS||sy>=pHS) continue;
                  uint p=pillHR.PixelGet(sx,sy);
                  uchar pa,pr,pg,pb;
                  RcdArgbSplit(p,pa,pr,pg,pb);
                  sumA+=pa;
                  if(pa>0){sumR+=pr; sumG+=pg; sumB+=pb; wc+=1.0;}
                 }
            uchar fa=(uchar)(sumA/ss2);
            if(fa>0&&wc>0)
              {
               uint blended=((uint)fa<<24)|((uint)(uchar)(sumR/wc)<<16)|((uint)(uchar)(sumG/wc)<<8)|(uint)(uchar)(sumB/wc);
               int dstX=pillX+px2, dstY=thumbY+py;
               if(dstX>=0&&dstX<rcdPanelWidth&&dstY>=0&&dstY<rcdBodyHeight)
                 {
                  uint ex=rcdCanvBlock.PixelGet(dstX,dstY);
                  rcdCanvBlock.PixelSet(dstX,dstY,RcdBlendPixel(ex,blended));
                 }
              }
           }
      pillHR.Destroy();
     }

   rcdCanvBlock.Update();
  }

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

При первом проходе отрисовываются цветные фоновые панели под абзацами блоков "Предупреждение", "Информация" и "Ответ". Функция проходит по массиву строк после разбиения текста в поисках строк блочного типа, а затем просматривает последующие строки, чтобы определить конец каждой непрерывной последовательности строк блока, учитывая пустые строки, которые могут находиться между строками одного и того же блока. Как только становится известен полный вертикальный размер блока, его верхняя и нижняя границы в пикселях вычисляются относительно текущего положения прокрутки и ограничиваются пределами видимой области. Затем в зависимости от типа блока и активной темы — тёмной или светлой — выбираются цвет заливки и цвет акцентной полосы слева, после чего фон и полоса отрисовываются на холсте блоков в виде залитых прямоугольников.

Во втором проходе перебираются все строки, полученные после разбиения текста, и отрисовывается их содержимое. Строки-заглушки изображений обрабатываются путем вывода кэшированных масштабированных пиксельных данных, центрированных по горизонтали в пределах текстовой области, с наложением каждого пикселя с помощью функции RcdBlendPixel и пропуском полностью прозрачных пикселей, при этом флаг "нарисовано" для каждого слота изображения предотвращает вывод одного и того же изображения более одного раза за проход. Заглушки логотипов обрабатываются аналогичным образом с использованием массива пикселей отображаемого холста области содержимого. Строки текста, целиком находящиеся за пределами видимой области прокрутки, пропускаются, а горизонтальное положение каждой видимой строки определяется по величине выступа и типу абзаца: блочные абзацы получают дополнительный отступ слева, чтобы освободить место для акцентной полосы, а в строках маркированного списка символ маркера выводится на холст отдельно в исходной позиции текста, после чего основной текст сдвигается вправо на ширину маркера. Цвет и шрифт текста по умолчанию выбираются в зависимости от типа абзаца, а для корректного восстановления альфа-канала определяется соответствующий цвет фона блока; затем строка выводится на холст либо по фрагментам текста с единым оформлением с учётом разметки, либо как обычный текст без разметки — в зависимости от наличия тегов разметки.

Наконец, скруглённый ползунок полосы прокрутки отрисовывается, если прокрутка активна и курсор мыши находится внутри области содержимого либо выполняется перетаскивание. Его вертикальное положение рассчитывается пропорционально текущему смещению прокрутки в пределах доступной высоты дорожки. Создаётся временный холст высокого разрешения, на котором с помощью функции RcdFillRoundRectHR  рисуется залитый прямоугольник с полностью скруглёнными углами; затем его изображение вручную уменьшается и попиксельно накладывается на холст блока, благодаря чему скруглённый ползунок полосы прокрутки получает плавные скруглённые концы со сглаженными краями. Цвет скруглённого ползунка полосы прокрутки меняется в зависимости от флагов взаимодействия, отражающих обычное состояние, наведение курсора или перетаскивание, а временный холст уничтожается до вывода содержимого холста блока на экран методом Update. Такая же логика применяется и к другим элементам макета. Теперь объединим все компоненты для первого запуска.

Отображение документа и инициализация программы

Теперь, когда системы отрисовки, разбиения текста на строки и работы с содержимым готовы, остаётся объединить их в процедуре отображения документа и в точке входа в программу.

//+------------------------------------------------------------------+
//| Show document — initialise state, canvases, and render           |
//+------------------------------------------------------------------+
void RcdShow()
  {
   //--- Prevent double-initialisation
   if(rcdIsActive) return;
   //--- Apply theme colors and compute layout geometry
   RcdApplyTheme(rcdTheme);
   RcdCalculateLayout();
   //--- Reset all interaction and display state
   rcdActiveTab        = RCD_TAB_LANGUAGE;
   rcdScrollPos        = 0;
   rcdDontShowAgain    = false;
   rcdIsDraggingSlider = false;
   rcdHoverClose       = false;
   rcdHoverOK          = false;
   rcdHoverCancel      = false;
   rcdHoverCheckbox    = false;
   rcdHoverSlider      = false;
   rcdMouseInBody      = false;
   rcdPrevMouseInBody  = false;
   rcdPrevMouseState   = 0;
   for(int i = 0; i < RCD_TAB_COUNT; i++) rcdHoverTabs[i] = false;
   //--- Create canvases; abort and clean up on failure
   if(!RcdCreateCanvases()) { RcdDestroyCanvases(); return; }
   //--- Load header logo and the large body-display logo
   rcdLogoLoaded = RcdLoadLogo();
   RcdLoadLogoDisplay();
   //--- Load all content images from embedded resources
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
     {
      rcdImgLoaded[ii]     = false;
      rcdImgCacheValid[ii] = false;
      RcdLoadImage(ii);
     }
   //--- Build documentation content on first show
   if(!rcdContentBuilt) RcdBuildContent();
   //--- Enable mouse move and wheel chart events
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   //--- Mark document active then render all sections
   rcdIsActive = true;
   RcdRebuildWrappedLines();
   RcdRenderAll();
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Enable mouse move and wheel events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
   //--- Show document on attach if requested by the input parameter
   if(rcdShowOnAttach)
     {
      RcdShow();
      Print("Rich Content Document: Displaying canvas-rendered documentation. Theme=",
            (rcdTheme == 0 ? "Dark" : "Light"));
     }
   else
     {
      Print("Rich Content Document: Attached. Set rcdShowOnAttach=true to display, or call RcdShow() manually.");
     }
   return(INIT_SUCCEEDED);
  }

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

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

В обработчике события OnInit на графике включаются события перемещения мыши и прокрутки колесика, а также проверяется значение входного параметра rcdShowOnAttach. Если значение равно true, немедленно вызывается функция RcdShow, и в журнал записывается сообщение с подтверждением, в котором указывается активная тема. Если значение равно false, вместо этого выводится сообщение, в котором пользователю объясняется, как отобразить документ вручную. В заключение обработчик события возвращает значение INIT_SUCCEEDED, подтверждая, что программа инициализирована правильно. После компиляции мы получаем следующий результат.

ОТРИСОВКА ПРИ ПЕРВОМ ЗАПУСКЕ

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

//+------------------------------------------------------------------+
//| Hide document — destroy canvases and free all resources          |
//+------------------------------------------------------------------+
void RcdHide()
  {
   if(!rcdIsActive) return;
   //--- Destroy all canvas objects and their chart labels
   RcdDestroyCanvases();
   //--- Free the scaled logo resource if it was created
   if(rcdLogoLoaded)
     {
      ResourceFree(rcdLogoScaledResName);
      rcdLogoLoaded = false;
     }
   //--- Reset image slot state and release pixel data arrays
   for(int ii = 0; ii < RCD_IMG_COUNT; ii++)
     {
      rcdImgLoaded[ii]     = false;
      rcdImgCacheValid[ii] = false;
     }
   ArrayResize(rcdImgPixels0, 0);
   ArrayResize(rcdImgPixels1, 0);
   ArrayResize(rcdImgPixels2, 0);
   ArrayResize(rcdImgPixels3, 0);
   ArrayResize(rcdImgPixels4, 0);
   //--- Release logo display pixel data
   rcdLogoDisplayReady = false;
   ArrayResize(rcdLogoDisplayPixels, 0);
   rcdIsActive = false;
   ChartRedraw();
  }

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy all canvases and release all resources cleanly
   RcdHide();
   Print("Rich Content Document: Deinitialized cleanly.");
  }

Функция RcdHide начинается с защитной проверки, аналогичной той, что используется в функции RcdShow, и немедленно завершает работу, если документ в данный момент не является активным. Затем она уничтожает все объекты холстов и удаляет соответствующие им метки с графика. Если при отображении панели был успешно создан ресурс масштабированного логотипа верхней панели, он освобождается с помощью функции ResourceFree, а флаг загрузки сбрасывается. Флаги всех пяти слотов изображений сбрасываются, а размеры соответствующих одномерных массивов пикселей устанавливаются равными нулю, освобождая память, в которой хранились данные масштабированных изображений. Массив пикселей для отображения логотипа также освобождается, а его флаг готовности сбрасывается. После освобождения всех ресурсов флаг активности устанавливается в значение false, и график перерисовывается, чтобы удалить панель с экрана.

Обработчик события OnDeinit устроен просто: он вызывает функцию RcdHide для полного освобождения ресурсов и выводит подтверждающее сообщение в журнал. Это гарантирует, что независимо от того, закрывает ли пользователь документ нажатием кнопки или программа удаляется с графика, вся выделенная память освобождается, а объекты графика удаляются, не оставляя неосвобождённых ресурсов. И, наконец, нам нужно обработать перемещение курсора мыши и щелчки по графику.

Обработка событий — взаимодействие, изменение размера и прокрутка

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

//+------------------------------------------------------------------+
//| Route and process all chart events for the document UI           |
//+------------------------------------------------------------------+
void RcdHandleChartEvent(const int eventId, const long &lParam, const double &dParam, const string &sParam)
  {
   if(!rcdIsActive) return;

   //--- Handle chart resize or reflow events
   if(eventId == CHARTEVENT_CHART_CHANGE)
     {
      //--- Recompute geometry and move all canvas objects to new positions
      RcdCalculateLayout();
      ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_YDISTANCE, rcdHeaderY);
      ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_YDISTANCE, rcdTabsY);
      ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_YDISTANCE, rcdBodyY);
      ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_YDISTANCE, rcdBodyY);
      ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_XDISTANCE, rcdPanelX);
      ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_YDISTANCE, rcdFooterY);
      //--- Resize canvases when the panel width has changed
      if(rcdCanvHeader.Width() != rcdPanelWidth)
        {
         rcdCanvHeader.Resize(rcdPanelWidth, RCD_HEADER_H);
         rcdCanvTabs.Resize(rcdPanelWidth, RCD_TABS_H);
         rcdCanvBody.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvBodyHR.Resize(rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS);
         rcdCanvBlock.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvFooter.Resize(rcdPanelWidth, RCD_FOOTER_H);
         ObjectSetInteger(0, rcdHeaderCanvasName, OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdTabsCanvasName,   OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBodyCanvasName,   OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_XSIZE, rcdPanelWidth);
         ObjectSetInteger(0, rcdBlockCanvasName,  OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdFooterCanvasName, OBJPROP_XSIZE, rcdPanelWidth);
        }
      else if(rcdCanvBody.Height() != rcdBodyHeight)
        {
         //--- Resize only the body canvases when height alone has changed
         rcdCanvBody.Resize(rcdPanelWidth, rcdBodyHeight);
         rcdCanvBodyHR.Resize(rcdPanelWidth * RCD_SS, rcdBodyHeight * RCD_SS);
         rcdCanvBlock.Resize(rcdPanelWidth, rcdBodyHeight);
         ObjectSetInteger(0, rcdBodyCanvasName,  OBJPROP_YSIZE, rcdBodyHeight);
         ObjectSetInteger(0, rcdBlockCanvasName, OBJPROP_YSIZE, rcdBodyHeight);
        }
      //--- Rewrap content and re-render everything after resize
      RcdRebuildWrappedLines();
      RcdRenderAll();
      ChartRedraw();
      return;
     }

   //--- Handle mouse move and click events
   if(eventId == CHARTEVENT_MOUSE_MOVE)
     {
      int mx     = (int)lParam;
      int my     = (int)dParam;
      int mstate = (int)sParam;
      //--- Snapshot previous hover state for dirty-region detection
      bool pClose = rcdHoverClose, pOK = rcdHoverOK, pCancel = rcdHoverCancel;
      bool pChk = rcdHoverCheckbox, pSL = rcdHoverSlider;
      bool pInBody = rcdMouseInBody;
      bool pTabs[RCD_TAB_COUNT];
      for(int i = 0; i < RCD_TAB_COUNT; i++) pTabs[i] = rcdHoverTabs[i];
      //--- Recompute all hover flags for the new mouse position
      RcdUpdateHovers(mx, my);
      //--- Toggle chart scroll lock when the mouse enters or leaves the body
      if(rcdMouseInBody != rcdPrevMouseInBody)
        {
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !rcdMouseInBody);
         rcdPrevMouseInBody = rcdMouseInBody;
        }
      //--- Compute which regions need to be redrawn
      bool hdrChanged    = (pClose != rcdHoverClose);
      bool tabsChanged   = false;
      bool footerChanged = (pOK != rcdHoverOK) || (pCancel != rcdHoverCancel) || (pChk != rcdHoverCheckbox);
      bool bodyChanged   = (pSL != rcdHoverSlider) || (pInBody != rcdMouseInBody);
      for(int i = 0; i < RCD_TAB_COUNT; i++)
         if(pTabs[i] != rcdHoverTabs[i]) tabsChanged = true;
      //--- Process mouse-down (button pressed) actions
      if(mstate == 1 && rcdPrevMouseState == 0)
        {
         //--- Close on click of close button or OK button
         if(rcdHoverClose || rcdHoverOK) { RcdHide(); rcdPrevMouseState = mstate; return; }
         //--- Close on click of Cancel button
         if(rcdHoverCancel)              { RcdHide(); rcdPrevMouseState = mstate; return; }
         //--- Toggle "don't show again" checkbox state
         if(rcdHoverCheckbox)
           {
            rcdDontShowAgain = !rcdDontShowAgain;
            footerChanged = true;
           }
         //--- Switch active tab on tab click
         for(int i = 0; i < RCD_TAB_COUNT; i++)
           {
            if(rcdHoverTabs[i] && i != rcdActiveTab)
              {
               rcdActiveTab = i;
               rcdScrollPos = 0;
               RcdRebuildWrappedLines();
               tabsChanged = true;
               bodyChanged = true;
               break;
              }
           }
         //--- Handle scrollbar track and pill interactions
         if(rcdScrollVisible && rcdMouseInBody)
           {
            int pillMargin = 4;
            int bLx = mx - rcdPanelX;
            int bLy = my - rcdBodyY;
            int pillX  = rcdPanelWidth - RCD_SB_MARGIN_R - RCD_SB_PILL_W;
            int trackH = rcdBodyHeight - pillMargin * 2;
            int thumbY = pillMargin;
            if(rcdMaxScroll > 0)
               thumbY = pillMargin + (int)(((double)rcdScrollPos / rcdMaxScroll) * (trackH - rcdSliderHeight));
            thumbY = MathMax(pillMargin, MathMin(rcdBodyHeight - rcdSliderHeight - pillMargin, thumbY));
            if(rcdHoverSlider)
              {
               //--- Begin dragging the scrollbar pill
               rcdIsDraggingSlider   = true;
               rcdDragStartMouseY    = bLy;
               rcdDragStartScrollPos = rcdScrollPos;
               ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
              }
            else if(RcdPointInRect(bLx, bLy, pillX-4, pillMargin, RCD_SB_PILL_W+8, trackH))
              {
               //--- Jump scroll to the clicked track position
               int newTop = bLy - pillMargin - rcdSliderHeight / 2;
               double ratio = (trackH - rcdSliderHeight > 0)
                  ? MathMax(0.0, MathMin(1.0, (double)newTop / (trackH - rcdSliderHeight))) : 0.0;
               rcdScrollPos = MathMax(0, MathMin(rcdMaxScroll, (int)MathRound(ratio * rcdMaxScroll)));
               bodyChanged = true;
              }
           }
        }
      else if(mstate == 1 && rcdPrevMouseState == 1 && rcdIsDraggingSlider)
        {
         //--- Update scroll position while dragging the pill
         int pillMargin = 4;
         int bLy  = my - rcdBodyY;
         int dy   = bLy - rcdDragStartMouseY;
         int travel = (rcdBodyHeight - pillMargin*2) - rcdSliderHeight;
         if(travel > 0)
           {
            int np = rcdDragStartScrollPos + (int)MathRound((double)dy / travel * rcdMaxScroll);
            np = MathMax(0, MathMin(rcdMaxScroll, np));
            if(np != rcdScrollPos) { rcdScrollPos = np; bodyChanged = true; }
           }
        }
      else if(mstate == 0 && rcdPrevMouseState == 1 && rcdIsDraggingSlider)
        {
         //--- Release scrollbar pill and restore chart scroll
         rcdIsDraggingSlider = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, !rcdMouseInBody);
         bodyChanged = true;
        }
      //--- Re-render only the dirty regions
      if(hdrChanged)    RcdRenderHeader();
      if(tabsChanged)   RcdRenderTabs();
      if(bodyChanged)   RcdRenderBody();
      if(footerChanged) RcdRenderFooter();
      if(hdrChanged || tabsChanged || bodyChanged || footerChanged) ChartRedraw();
      rcdPrevMouseState = mstate;
      return;
     }

   //--- Handle mouse wheel scroll events
   if(eventId == CHARTEVENT_MOUSE_WHEEL)
     {
      if(!rcdScrollVisible) return;
      int delta = (int)dParam;
      int mx    = (int)(short)lParam;
      int my    = (int)(short)(lParam >> 16);
      int bLx   = mx - rcdPanelX;
      int bLy   = my - rcdBodyY;
      //--- Only scroll when the wheel event originates inside the body area
      if(RcdPointInRect(bLx, bLy, 0, 0, rcdPanelWidth, rcdBodyHeight))
        {
         int step = 3 * rcdLineHeight;
         //--- Scroll up on positive delta, down on negative
         rcdScrollPos += (delta > 0 ? -step : step);
         rcdScrollPos = MathMax(0, MathMin(rcdMaxScroll, rcdScrollPos));
         RcdRenderBody();
         ChartRedraw();
        }
     }
  }

//+------------------------------------------------------------------+
//| Chart event function                                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Route all chart events to the document's central event handler
   RcdHandleChartEvent(id, lparam, dparam, sparam);
  }

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

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

При нажатии кнопки мыши кнопка "Закрыть" в верхней панели, а также кнопки "ОК" и "Отмена" в нижней панели вызывают функцию RcdHide для закрытия документа. Щелчок по вкладке делает её активной, сбрасывает позицию прокрутки и заново формирует строки после разбиения текста. Щелчок по скруглённому ползунку полосы прокрутки запускает перетаскивание с сохранением начальной позиции курсора и смещения прокрутки, а щелчок по дорожке полосы прокрутки вне ползунка сразу изменяет позицию прокрутки пропорционально месту щелчка. Во время перетаскивания новая позиция прокрутки вычисляется по смещению курсора от начальной точки с пропорциональным пересчётом относительно доступного хода ползунка по дорожке. При отпускании кнопки мыши состояние перетаскивания сбрасывается, и прокрутка графика восстанавливается.

События колёсика мыши обрабатываются только тогда, когда курсор находится в области содержимого. Каждый шаг колёсика прокручивает содержимое на три высоты строки в направлении, определяемом знаком дельты; позиция прокрутки ограничивается допустимым диапазоном, после чего область содержимого перерисовывается. Сам обработчик события OnChartEvent содержит всего одну строку — перенаправление всех событий непосредственно в функцию RcdHandleChartEvent. На этом мы завершаем выполнение поставленных задач. Далее проведём тестирование программы на исторических данных.


Тестирование на исторических данных

Мы подключили программу к графику в MetaTrader 5, чтобы убедиться, что документ отображается правильно и все интерактивные элементы работают так, как и ожидалось. Ниже показан результат в виде одного изображения в формате Graphics Interchange Format (GIF).

GIF-анимация тестирования на исторических данных

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


Заключение

Итак, мы создали для программ MQL5 систему документации с расширенным форматированием, полностью отрисовываемую на холсте, — встроенное в график руководство с прокруткой, вкладками и форматированием, которое любая программа может использовать для предоставления профессиональной, самодостаточной документации непосредственно в MetaTrader 5. Начав с простого мастера настройки, который мы создали в части 9, мы прошли долгий путь — от простых текстовых меток на графике до конвейера попиксельной отрисовки, легковесной системы встроенной разметки, фигур со сглаживанием методом суперсэмплинга, масштабирования изображений с бикубической интерполяцией и полностью интерактивного интерфейса, управляемого событиями. В результате работа с документацией напоминает открытие PDF-файла или документа Word, но сама документация отображается непосредственно на графике и при компиляции включается в файл программы, которую она описывает. Прочитав эту статью, вы сможете:

  • Встроить в любую создаваемую вами программу на MQL5 панель документации с полноценным форматированием, прокруткой и вкладками, заменив руководства в виде обычного текста без разметки документацией с расширенным форматированием
  • Создавать содержимое документа с помощью системы встроенной разметки, используя полужирное и курсивное начертание, цвет текста, выделение фоном и структурированные абзацы, которые по качеству отображения не уступают документам, созданным в текстовом редакторе
  • Вынести движок документации в отдельный заголовочный файл и подключите его с помощью директивы #include: так основной файл программы будет отвечать за её основную логику, а руководство будет подключаться на этапе компиляции


Вложения

№ п/п
Имя
Тип
Описание
1 Rich Content Document.mq5 Основной файл советника Основной файл, реализующий логику работы с документацией
2 Rich Content Document BMP files.zip ZIP-архив Содержит файлы изображений в формате BMP, на которые есть ссылки


Проект поддерживается на платформе Algo Forge: https://forge.mql5.io/29210372/Article-22303-Rich-Content-Document-MQL5-Canvas

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

Прикрепленные файлы |
Алгоритм оптимизации, вдохновлённый охотой лисы —  Fox-inspired Optimization Algorithm (FOX) Алгоритм оптимизации, вдохновлённый охотой лисы — Fox-inspired Optimization Algorithm (FOX)
Разбираем FOX (Fox-inspired Optimization Algorithm): реализуем алгоритм в MQL5, детально проверяем ветви "прыжка" и "блуждания" и показываем, как притяжение к нулю и фиксированный масштаб шага мешают поиску в торговых диапазонах. Пошагово измеряем вклад каждого механизма на тестовом стенде и объясняем расхождения формул и кода. В конце — готовый к использованию класс C_AO_FOX и исправленная версия с шагом в долях диапазона.
От новичка до эксперта: Адаптивное управление рисками в стратегиях на основе ликвидности От новичка до эксперта: Адаптивное управление рисками в стратегиях на основе ликвидности
В этой статье мы рассмотрим практические и надежные методы управления рисками, специально адаптированные для торговли на основе ликвидности. Вы научитесь защищать позиции во время ретестов, уверенно работать с ложными пробоями и распознавать признаки возможных манипуляций вблизи уровней. К концу статьи вы создадите адаптивный советник со встроенным контролем риска, способный управлять переворотами зон и исполнять стратегически обоснованные отложенные ордера.
Создание системы торговой аналитики (Часть 1): Основы и архитектура системы Создание системы торговой аналитики (Часть 1): Основы и архитектура системы
Мы проектируем простой внешний пайплайн аналитики торговых операций для MetaTrader 5 и реализуем его бэкенд на Python с использованием Flask и SQLite. В статье определяются архитектура, модель данных и API с поддержкой версий, а также показано, как настроить среду, инициализировать базу данных и локально запустить сервер. В результате вы получаете понятную основу для сбора записей о закрытых торговых операциях из MetaTrader 5 и их хранения для последующего анализа.
От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5 От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5
В этой статье представлен индикатор на языке MQL5, который обнаруживает перевороты зон ликвидности и управляет ими. Он выявляет зоны предложения и спроса на старших таймфреймах с помощью паттерна "база → импульс", применяет формализованные пороговые значения для пробоя и силы импульса и автоматически переворачивает зоны при изменении структуры. В результате получается динамическая карта поддержки и сопротивления, которая сокращает необходимость ручной перерисовки и дает понятный и практически применимый контекст для оценки сигналов и ретестов.