Торговые инструменты MQL5 (Часть 33): Создание системы документации с расширенным форматированием для программ на языке MQL5
Введение
Представьте, что вы подключаете свою программу к графику и открываете в MetaTrader 5 (MetaTrader 5) понятный документ с вкладками и возможностью прокрутки. Он содержит стилизованные заголовки, цветной текст, встроенные изображения и интерактивную навигацию. Никаких внешних файлов и неработающих ссылок. Не нужно гадать. Именно это мы и реализовали в данной статье. В части 9 этой серии мы разработали мастер настройки для советников на языке MetaQuotes Language 5 (MQL5), в котором используются стандартные объекты графика и прокручиваемое текстовое руководство. В части 32 мы дополнили палитру инструментов прицельной сеткой в виде перекрестия и лупой.
Часть 33 непосредственно развивает эти наработки. Здесь мы идём гораздо дальше, с нуля переосмысливая подход к документации с помощью класса CCanvas. Это позволяет создать систему документации с расширенным форматированием, полностью отрисовываемую на холсте. Она поддерживает вкладки, форматирование фрагментов текста с помощью разметки, встроенные изображения, тематические палитры, отрисовку со сглаживанием методом суперсэмплинга и прокручиваемую область содержимого. Эти возможности позволяют разработчикам MQL5 и алгоритмическим трейдерам размещать автономную документацию непосредственно внутри своих программ. Мы рассмотрим следующие темы:
- Новый подход к документации, встроенной в график: концепция и дизайн
- Реализация на языке MQL5
- Тестирование на исторических данных
- Заключение
В итоге у вас будет готовый движок документации для многократного использования, который можно будет встроить в любую создаваемую вами программу на языке 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 ¶Array[], 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 ¶Array[], 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 ¶Array[], 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 ¶Array[], 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).

В ходе тестирования документ автоматически открывался со светлой темой при подключении программы к графику; при переключении вкладок каждый раздел содержимого загружался корректно, а позиция прокрутки каждый раз сбрасывалась; скруглённый ползунок полосы прокрутки точно следовал за перемещением мыши при перетаскивании, обеспечивая прокрутку по всей высоте содержимого.
Заключение
Итак, мы создали для программ 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
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Алгоритм оптимизации, вдохновлённый охотой лисы — Fox-inspired Optimization Algorithm (FOX)
От новичка до эксперта: Адаптивное управление рисками в стратегиях на основе ликвидности
Создание системы торговой аналитики (Часть 1): Основы и архитектура системы
От новичка до эксперта: Выявление переворотов зон ликвидности на языке MQL5
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования