Торговые инструменты на MQL5 (Часть 16): Улучшенное сглаживание методом суперсэмплинга (SSAA) и рендеринг в высоком разрешении
Введение
В своей предыдущей статье (Часть 15) мы разработали панель на базе canvas в MetaQuotes Language 5 (MQL5), которая включает в себя эффекты размытия, рендеринг теней и плавную прокрутку колеса мыши для интерактивного мониторинга рынка, включая визуальные панели и настраиваемые темы оформления. В Части 16 мы улучшим панель с помощью методов сглаживания и рендеринга с высоким разрешением, используя суперсэмплинг для получения более плавной графики, рамок и элементов. Это усовершенствование включает в себя области canvas с высоким разрешением для статистики и текстовые панели, точные функции рисования округлых форм и улучшенную интерактивность для улучшения визуального качества. В статье рассмотрим следующие темы:
- Изучение методов сглаживания и рендеринга с высоким разрешением
- Реализация средствами MQL5
- Тестирование на истории
- Заключение
К концу статьи у вас будет обновленная панель на MQL5 с улучшенным рендерингом для более четкой и профессиональной визуализации рынка. Давайте приступим!
Изучение методов сглаживания и рендеринга с высоким разрешением
Фреймворк сглаживания и рендеринга с высоким разрешением устраняет визуальные артефакты в цифровой графике, такие как рваные края или "зубчатость изображения", возникающие, когда непрерывные фигуры отображаются на дискретной пиксельной сетке. Зубчатость проявляется в виде ступенчатых паттернов на линиях, кривых или рамках, снижая четкость и профессионализм отображения, например, на наших торговых панелях. Для смягчения такого эффекта, такие методы, как суперсэмплинг, позволяют визуализировать сцены с более высоким разрешением — обычно кратным целевому размеру, — а затем понижать дискретизацию путем усреднения значений пикселей, сглаживания краев с помощью смешивания цветов и создания более естественного внешнего вида. Ниже приведен пример процесса суперсэмплинга.

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

Реализация средствами MQL5
Чтобы усовершенствовать программу на MQL5, нам нужно будет добавить новые определения, глобальные переменные и входные параметры для управления новыми улучшениями, такими как показаны ниже, для рендеринга в высоком разрешении и возможности суперсэмплинга.
//+------------------------------------------------------------------+ //| Canvas Dashboard PART4.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 // Added two new canvas objects for high-resolution rendering //+------------------------------------------------------------------+ //| Canvas objects | //+------------------------------------------------------------------+ CCanvas canvasGraph; //--- Declare graph canvas object CCanvas canvasStats; //--- Declare stats canvas object CCanvas canvasStatsHighRes; //--- Declare stats high-res canvas object CCanvas canvasHeader; //--- Declare header canvas object CCanvas canvasText; //--- Declare text canvas object CCanvas canvasTextHighRes; //--- Declare text high-res canvas object // Added names for the new high-resolution canvases //+------------------------------------------------------------------+ //| Canvas names | //+------------------------------------------------------------------+ string canvasGraphName = "GraphCanvas"; //--- Set graph canvas name string canvasStatsName = "StatsCanvas"; //--- Set stats canvas name string canvasStatsHighResName = "StatsCanvasHighRes"; //--- Set stats high-res name string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name string canvasTextName = "TextCanvas"; //--- Set text canvas name string canvasTextHighResName = "TextCanvasHighRes"; //--- Set text high-res name // New group sinput group "=== TEXT PANEL SETTINGS ===" input int TriangleRoundRadius = 1; // Triangle Round Radius input double TriangleBaseWidthPercent = 65.0; // Triangle Base Width Percent (of button size) input double TriangleHeightPercent = 70.0; // Triangle Height Percent (of base width) input bool ShowUpDownButtons = false; // Show Up/Down Buttons input int TextFontSize = 17; // Text Font Size // Added a new global variable for supersampling factor const int smoothness_factor = 10; // Higher = smoother drag (e.g., 20 for more) const int supersamplingFactor = 4; // Supersampling for smooth rounds
Сначала мы расширяем объекты canvas, объявляя дополнительные версии с высоким разрешением для панелей статистики и текстовых панелей, в частности "canvasStatsHighRes" и "canvasTextHighRes", наряду с существующими "canvasGraph", "canvasStats", "canvasHeader" и "canvasText" для поддержки рендеринга с использованием суперсэмплирования. Затем мы также определяем соответствующие имена для этих новых canvas с высоким разрешением, добавляя для "canvasStatsHighResName" значение "StatsCanvasHighRes", а для "canvasTextHighResName" значение "TextCanvasHighRes", которые будут использоваться для идентификации и создания в программе. Для большей ясности мы выделили конкретные изменения.
В новой группе ввода с меткой "=== TEXT PANEL SETTINGS ===" мы вводим настраиваемые пользователем параметры, включая "TriangleRoundRadius" для управления округлением углов стрелок, "TriangleBaseWidthPercent" и "TriangleHeightPercent" для настройки пропорций стрелок относительно размера кнопки, "ShowUpDownButtons" для переключения видимости кнопок прокрутки, и "TextFontSize" для настройки размера отображаемого текста. В частности, мы хотим обновить полосу прокрутки, чтобы она соответствовала новой версии Windows 11, последней версии от 2026 года. Наконец, мы определяем две константы: "smoothness_factor", значение которых инициализируется равным 10 для более плавного взаимодействия при прокрутке, и "supersamplingFactor", значение которого устанавливается равным 4 для обеспечения рендеринга с высоким разрешением путем умножения размеров пикселей для сглаживания перед понижением дискретизации.
Представьте, что вы рисуете все размером в 4 раза больше, а затем уменьшаете, чтобы изображение стало очень плавным. Это является ключевым фактором для сглаживания. Рисуя в 4-кратном разрешении (больше), мы можем усреднять количество пикселей при уменьшении, удаляя неровные края кривых, рамок и текста. Это улучшает общее качество графики, но потребляет больше ресурсов компьютера. Поэтому вам может понадобиться использовать этот инструмент с осторожностью. Теперь, в обработчике OnInit мы создадим эти canvas с высоким разрешением следующим образом, помимо обычных.
if (EnableStatsPanel) { //--- Check stats panel enabled int statsX = currentCanvasX + currentWidth + PanelGap; //--- Compute stats X if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats canvas Print("Failed to create Stats Canvas"); //--- Log creation failure } if (!canvasStatsHighRes.Create(canvasStatsHighResName, (currentWidth / 2) * supersamplingFactor, currentHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats high-res Print("Failed to create Stats High-Res Canvas"); //--- Log creation failure } statsCreated = true; //--- Set stats created flag } if (EnableTextPanel) { //--- Check text panel enabled int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y int text_width = inner_header_width; //--- Set text width int text_height = TextPanelHeight; //--- Set text height if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, text_width, text_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text canvas Print("Failed to create Text Canvas"); //--- Log creation failure } if (!canvasTextHighRes.Create(canvasTextHighResName, text_width * supersamplingFactor, text_height * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text high-res Print("Failed to create Text High-Res Canvas"); //--- Log creation failure } textCreated = true; //--- Set text created flag }
В обработчике OnInit мы проверяем, включена ли панель статистики с помощью входного параметра "EnableStatsPanel", и если да, мы вычисляем положение X для canvas статистики, добавляя текущее значение X canvas, ширину и зазор между панелями. Затем мы создаем стандартный canvas статистики, используя метод "CreateBitmapLabel" в объекте "canvasStats", указывая идентификатор графика, подокно, название, положение, размеры и цветовой формат COLOR_FORMAT_ARGB_NORMALIZE, при этом выводим сообщение об ошибке, если создание завершается неудачно, точно так же, как мы делали с предыдущей версией. Затем мы создаем canvas статистики с высоким разрешением с помощью метода Create для объекта "canvasStatsHighRes", используя имя с высоким разрешением, масштабированную ширину путём умножения половины текущей ширины на "supersamplingFactor", масштабированную высоту аналогичным образом и тот же цветовой формат, снова выводя ошибку, прежде чем установить флаг "statsCreated" в значение true.
Аналогично, если текстовая панель включена с помощью параметра "EnableTextPanel", мы вычисляем положение по оси Y для текстового canvas, складывая текущее значение по оси Y класса canvas, высоту заголовка, отступы, текущую высоту и отступ панели, а затем устанавливаем ширину текста в соответствии с шириной и высотой внутреннего заголовка, равными значению параметра "TextPanelHeight". Мы создаём стандартный текстовый canvas с помощью функции "CreateBitmapLabel" для объекта "canvasText", обеспечивая необходимые параметры и выводя сообщения об ошибке. Затем мы создаём текстовый canvas высокого разрешения, используя функцию "Create" в "canvasTextHighRes" с масштабированными размерами на основе параметра «supersamplingFactor», регистрируем в лог любые проблемы, а затем устанавливаем флаг «textCreated» в значение true. Поскольку мы создали новые объекты canvas, необходимо рассмотреть возможность их уничтожения, когда они не нужны в соответствующих местах, так же, как и в случае с обычными объектами. Например, при деинициализации мы используем следующую логику.
//+------------------------------------------------------------------+ //| Deinitialize expert | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { // Deinitialize expert advisor canvasHeader.Destroy(); //--- Destroy header canvas if (graphCreated) canvasGraph.Destroy(); //--- Destroy graph if created if (statsCreated) { canvasStats.Destroy(); //--- Destroy stats if created canvasStatsHighRes.Destroy(); //--- Destroy stats high-res } if (textCreated) { canvasText.Destroy(); //--- Destroy text if created canvasTextHighRes.Destroy(); //--- Destroy text high-res } ChartRedraw(); //--- Redraw chart }
Мы уничтожаем объекты canvas с высоким разрешением только тогда, когда они не нужны. Мы используем тот же формат в функциях "ToggleMinimize" и "CloseDashboard". Далее нам нужно будет определить логику рисования новых причудливых закругленных прямоугольников для заголовков статистики и закругленных треугольников для кнопок полосы прокрутки, поскольку мы хотим отказаться от статичных и нарисовать свои собственные. Мы используем следующую логику.
//+------------------------------------------------------------------+ //| Draw rectangle corner arc with exact boundaries | //+------------------------------------------------------------------+ void DrawRectCornerArcPrecise(CCanvas &cvs, int centerX, int centerY, int radius, int thickness, uint borderARGB, double startAngle, double endAngle) { int halfThickness = thickness / 2; double outerRadius = (double)radius + halfThickness; double innerRadius = (double)radius - halfThickness; if(innerRadius < 0) innerRadius = 0; int pixelRange = (int)(outerRadius + 2); for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); if(distance < innerRadius || distance > outerRadius) continue; double angle = MathArctan2((double)deltaY, (double)deltaX); if(IsAngleBetween(angle, startAngle, endAngle)) cvs.PixelSet(centerX + deltaX, centerY + deltaY, borderARGB); } } } //+------------------------------------------------------------------+ //| Fill quadrilateral | //+------------------------------------------------------------------+ void FillQuadrilateral(CCanvas &cvs, double &verticesX[], double &verticesY[], uint fillColor) { double minY = verticesY[0], maxY = verticesY[0]; for(int i = 1; i < 4; i++) { if(verticesY[i] < minY) minY = verticesY[i]; if(verticesY[i] > maxY) maxY = verticesY[i]; } int yStart = (int)MathCeil(minY); int yEnd = (int)MathFloor(maxY); for(int y = yStart; y <= yEnd; y++) { double scanlineY = (double)y + 0.5; double xIntersections[8]; int intersectionCount = 0; for(int i = 0; i < 4; i++) { int nextIndex = (i + 1) % 4; double x0 = verticesX[i], y0 = verticesY[i]; double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex]; double edgeMinY = (y0 < y1) ? y0 : y1; double edgeMaxY = (y0 > y1) ? y0 : y1; if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; if(MathAbs(y1 - y0) < 1e-12) continue; double interpolationFactor = (scanlineY - y0) / (y1 - y0); if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; xIntersections[intersectionCount++] = x0 + interpolationFactor * (x1 - x0); } for(int a = 0; a < intersectionCount - 1; a++) for(int b = a + 1; b < intersectionCount; b++) if(xIntersections[a] > xIntersections[b]) { double temp = xIntersections[a]; xIntersections[a] = xIntersections[b]; xIntersections[b] = temp; } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { int xLeft = (int)MathCeil(xIntersections[pairIndex]); int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); for(int x = xLeft; x <= xRight; x++) cvs.PixelSet(x, y, fillColor); } } } //+------------------------------------------------------------------+ //| Normalize angle | //+------------------------------------------------------------------+ double NormalizeAngle(double angle) { double twoPi = 2.0 * M_PI; angle = MathMod(angle, twoPi); if(angle < 0) angle += twoPi; return angle; } //+------------------------------------------------------------------+ //| Is angle between | //+------------------------------------------------------------------+ bool IsAngleBetween(double angle, double startAngle, double endAngle) { angle = NormalizeAngle(angle); startAngle = NormalizeAngle(startAngle); endAngle = NormalizeAngle(endAngle); double span = NormalizeAngle(endAngle - startAngle); double relativeAngle = NormalizeAngle(angle - startAngle); return relativeAngle <= span; }
Здесь мы создаем функцию "DrawRectCornerArcPrecise" для отображения точных угловых дуг для закругленных рамок прямоугольника на объекте canvas с высоким разрешением, вычисляем половину толщины, чтобы определить внутренний и внешний радиусы, затем выполняем перебор по диапазону пикселей вокруг центра, чтобы проверить расстояния и углы, устанавливая пиксели только в том случае, если они попадают в пределы сегмента дуги, определяемого начальным и конечным углами. Мы реализуем функцию "FillQuadrilateral" для заполнения произвольных четырехугольных форм, используя алгоритм сканирования строк, сначала определяя минимальные и максимальные границы Y от вершин, затем для каждой строки сканирования по оси Y вычисляя пересечения ребер, сортируя их и заполняя горизонтальные промежутки между парами пересечений, чтобы обеспечить полное покрытие без пробелов.
Мы определяем функцию "NormalizeAngle" для стандартизации углов в диапазоне от 0 до 2 * pi с помощью операции взятия остатка от деления и корректировки отрицательных значений. Эта стандартизация обеспечивает единообразное сравнение углов в круговых геометрических формах, таких как дуги. Мы добавляем функцию "IsAngleBetween", чтобы определить, находится ли данный угол между начальным и конечным углами. Мы нормализуем все входные параметры, вычисляем линейную оболочку и проверяем относительное положение, поддерживая движение по часовой или против часовой стрелки для гибкого отображения дуг. Теперь можно использовать эти функции для рисования скругленного прямоугольника, как показано ниже.
//+------------------------------------------------------------------+ //| Fill rounded rectangle | //+------------------------------------------------------------------+ void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color) { // Render rounded fill if (radius <= 0) { //--- Check zero radius cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color); //--- Fill rectangle return; //--- Exit function } radius = MathMin(radius, MathMin(w / 2, h / 2)); //--- Adjust radius cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc cvs.FillCircle(x + radius, y + radius, radius, argb_color); //--- Fill top-left circle cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color); //--- Fill top-right circle cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color); //--- Fill bottom-right circle cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color); //--- Fill bottom-left circle cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal body cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical body } //+------------------------------------------------------------------+ //| Draw rounded rectangle border | //+------------------------------------------------------------------+ void DrawRoundedRectangleBorderHiRes(CCanvas &cvs, int positionX, int positionY, int width, int height, int radius, uint borderColorARGB, int thickness) { int scaledThickness = thickness; DrawRectStraightEdge(cvs, positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB); DrawRectStraightEdge(cvs, positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB); DrawRectStraightEdge(cvs, positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB); DrawRectStraightEdge(cvs, positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB); DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); } //+------------------------------------------------------------------+ //| Draw straight edge for rectangle border | //+------------------------------------------------------------------+ void DrawRectStraightEdge(CCanvas &cvs, double startX, double startY, double endX, double endY, int thickness, uint borderARGB) { double deltaX = endX - startX; double deltaY = endY - startY; double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); if(edgeLength < 1e-6) return; double perpendicularX = -deltaY / edgeLength; double perpendicularY = deltaX / edgeLength; double edgeDirectionX = deltaX / edgeLength; double edgeDirectionY = deltaY / edgeLength; double halfThickness = (double)thickness / 2.0; double extensionLength = 1.5; double extendedStartX = startX - edgeDirectionX * extensionLength; double extendedStartY = startY - edgeDirectionY * extensionLength; double extendedEndX = endX + edgeDirectionX * extensionLength; double extendedEndY = endY + edgeDirectionY * extensionLength; double verticesX[4], verticesY[4]; verticesX[0] = extendedStartX - perpendicularX * halfThickness; verticesY[0] = extendedStartY - perpendicularY * halfThickness; verticesX[1] = extendedStartX + perpendicularX * halfThickness; verticesY[1] = extendedStartY + perpendicularY * halfThickness; verticesX[2] = extendedEndX + perpendicularX * halfThickness; verticesY[2] = extendedEndY + perpendicularY * halfThickness; verticesX[3] = extendedEndX - perpendicularX * halfThickness; verticesY[3] = extendedEndY - perpendicularY * halfThickness; FillQuadrilateral(cvs, verticesX, verticesY, borderARGB); }
Чтобы создать скругленный прямоугольник, мы сначала реализуем функцию "FillRoundedRectangle" для отображения закругленных прямоугольников с заливкой на объекте canvas, сначала обрабатывая случаи с нулевым радиусом, заполняя стандартный прямоугольник, затем регулируя радиус так, чтобы он соответствовал половине ширины или высоты, рисуя дуги в четверть дуги в каждом углу, используя метод Arc с преобразованием градусов в радианы, заливая полные круги в углах для полного покрытия и, наконец, заливая горизонтальные и вертикальные участки тела, чтобы плавно соединить закругленные части.
Затем мы создаём функцию "DrawRoundedRectangleBorderHiRes" для высокоточной отрисовки рамок вокруг скругленных прямоугольников, масштабируем толщину, вызываем функцию "DrawRectStraightEdge" для отрисовки четырёх прямых сторон и используем функцию "DrawRectCornerArcPrecise" для каждой угловой дуги с заданными начальными и конечными углами в радианах (например, от M_PI до M_PI * 1,5 для верхнего левого угла), обеспечивая точные, сглаженные контуры без наложений.
Наконец, в функции "DrawRectStraightEdge" мы вычисляем дельты и нормализуем векторы для направления и перпендикуляра ребер, вычисляем половину толщины для ширины рамки, слегка удлиняем линию за пределы конечных точек, чтобы обеспечить плавные соединения углов, определяем четыре вершины для четырехугольной полосы вдоль ребра и передаем их в функцию "FillQuadrilateral" для заполнения сегмента рамки, создавая непрерывную, высококачественную прямую линию с помощью векторной геометрии. Это функции, которые мы будем вызывать для создания желаемого скругленного прямоугольника. Теперь можем определить логику округленного треугольника. Сначала нам понадобятся вспомогательные функции.
//+------------------------------------------------------------------+ //| Precompute triangle geometry | //+------------------------------------------------------------------+ void PrecomputeTriangleGeometry(double &sharpX[], double &sharpY[], int radius, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) { for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { int previousIndex = (cornerIndex + 2) % 3; int nextIndex = (cornerIndex + 1) % 3; double edgeA_X = sharpX[cornerIndex] - sharpX[previousIndex], edgeA_Y = sharpY[cornerIndex] - sharpY[previousIndex]; double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); edgeA_X /= edgeA_Length; edgeA_Y /= edgeA_Length; double edgeB_X = sharpX[nextIndex] - sharpX[cornerIndex], edgeB_Y = sharpY[nextIndex] - sharpY[cornerIndex]; double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); edgeB_X /= edgeB_Length; edgeB_Y /= edgeB_Length; double normalA_X = edgeA_Y, normalA_Y = -edgeA_X; double normalB_X = edgeB_Y, normalB_Y = -edgeB_X; double bisectorX = normalA_X + normalB_X, bisectorY = normalA_Y + normalB_Y; double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } bisectorX /= bisectorLength; bisectorY /= bisectorLength; double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; if(cosInteriorAngle > 1.0) cosInteriorAngle = 1.0; if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0; double halfAngle = MathArccos(cosInteriorAngle) / 2.0; double sinHalfAngle = MathSin(halfAngle); if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12; double distanceToCenter = radius / sinHalfAngle; arcCentersX[cornerIndex] = sharpX[cornerIndex] + bisectorX * distanceToCenter; arcCentersY[cornerIndex] = sharpY[cornerIndex] + bisectorY * distanceToCenter; double deltaX_A = sharpX[cornerIndex] - sharpX[previousIndex], deltaY_A = sharpY[cornerIndex] - sharpY[previousIndex]; double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; double interpolationFactor_A = ((arcCentersX[cornerIndex] - sharpX[previousIndex])*deltaX_A + (arcCentersY[cornerIndex] - sharpY[previousIndex])*deltaY_A) / lengthSquared_A; tangentX[cornerIndex][1] = sharpX[previousIndex] + interpolationFactor_A * deltaX_A; tangentY[cornerIndex][1] = sharpY[previousIndex] + interpolationFactor_A * deltaY_A; double deltaX_B = sharpX[nextIndex] - sharpX[cornerIndex], deltaY_B = sharpY[nextIndex] - sharpY[cornerIndex]; double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; double interpolationFactor_B = ((arcCentersX[cornerIndex] - sharpX[cornerIndex])*deltaX_B + (arcCentersY[cornerIndex] - sharpY[cornerIndex])*deltaY_B) / lengthSquared_B; tangentX[cornerIndex][0] = sharpX[cornerIndex] + interpolationFactor_B * deltaX_B; tangentY[cornerIndex][0] = sharpY[cornerIndex] + interpolationFactor_B * deltaY_B; startAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][1] - arcCentersY[cornerIndex], tangentX[cornerIndex][1] - arcCentersX[cornerIndex]); endAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][0] - arcCentersY[cornerIndex], tangentX[cornerIndex][0] - arcCentersX[cornerIndex]); } } //+------------------------------------------------------------------+ //| Angle in arc sweep for triangle | //+------------------------------------------------------------------+ bool TriangleAngleInArcSweep(double startAngle, double endAngle, double angle) { double twoPi = 2.0 * M_PI; double startAngleMod = MathMod(startAngle + twoPi, twoPi); double endAngleMod = MathMod(endAngle + twoPi, twoPi); angle = MathMod(angle + twoPi, twoPi); double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); if(ccwSpan <= M_PI) { double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); return(relativeAngle <= ccwSpan + 1e-6); } else { double cwSpan = twoPi - ccwSpan; double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); return(relativeAngle <= cwSpan + 1e-6); } }
Мы определяем функцию "PrecomputeTriangleGeometry" для вычисления необходимых геометрических элементов для отображения скругленных треугольников, таких как те, которые мы будем использовать в наших пользовательских значках стрелок, путем перебора каждого из трех углов с помощью цикла с параметром "cornerIndex" от 0 до 2. Для каждого угла мы определяем предыдущий и следующий индексы по модулю 3 для циклического доступа, вычисляем нормализованные векторы ребер от острых вершин "sharpX" и "sharpY" до смежных точек и выводим внешние нормали, поворачивая эти векторы на 90 градусов. Затем формируем биссектрису угла, суммируя нормали, нормализуя ее после обработки случаев с почти нулевой длиной, и вычисляем косинус внутреннего угла с помощью скалярного произведения отрицательных векторов ребер, ограничивая его значение диапазоном от -1 до 1, прежде чем найти половинный угол и его синус, чтобы избежать деления на ноль.
Используя радиус, деленный на этот синус, мы располагаем центр дуги вдоль биссектрисы от угла, обеспечивая равномерное скругление; затем проецируем центр дуги на ребра, чтобы найти точки касания, хранящиеся в "tangentX" и "tangentY", и вычисляем начальный и конечный углы с помощью MathArctan2 для определения направления и угла движения дуги в этом углу.
Этот предварительный расчет имеет решающее значение для точного рендеринга округлых форм со сглаживанием, поскольку он преобразует острые вершины треугольников в плавные кривые, определяя центры дуг "arcCentersX" и "arcCentersY", касательные и углы "startAngles" и "endAngles", которые позволяют выполнять векторную заливку без пиксельных искажений, что значительно улучшает визуальное качество элементов пользовательского интерфейса, таких как стрелки прокрутки, благодаря возможности точной проверки границ во время итерации пикселей.
Затем реализуем функцию "TriangleAngleInArcSweep", чтобы проверить, попадает ли данный угол в дугу, определяемую начальным и конечным углами, предварительно нормализуя все углы до значений от 0 до 2 * pi с помощью MathMod и добавляя 2 * pi для положительных значений. Мы вычисляем хорду против часовой стрелки и, если она равна pi или меньше, проверяем относительный угол от начала; в противном случае используем хорду по часовой стрелке и проверяем от конца, добавляя небольшую эпсилон для точности с плавающей запятой, которая поддерживает оба направления дуги и обеспечивает корректное включение при заполнении методом сканирования строк или тестировании пикселей во время отрисовки закругленных треугольников. В принципе, если вам интересно, что это за алгоритм сканирования строк, мы немного объясним, что это такое, чтобы у вас было понимание.
Этот алгоритм обрабатывает изображение слева направо, сканируя по одной горизонтальной строке за раз, а не обрабатывая отдельные пиксели. Он фиксирует все точки пересечения ребер вдоль каждой строки сканирования и заполняет многоугольник, раскрашивая области между парами пересечений. Это можно сравнить с проведением прямой линии поперек фигуры на бумаге одной ручкой: начиная от левой границы и двигаясь вправо, вы рисуете непрерывно, но всякий раз, когда сталкиваетесь с пересечением с границей многоугольника, вы соответственно останавливаете или возобновляете рисование. Алгоритм работает по тому же принципу. На рисунке ниже проиллюстрировано это поведение: красные точки представляют вершины многоугольника, в то время как синие точки указывают точки пересечения вдоль линии сканирования.

Надеюсь, что это понятно. Теперь можно перейти к определению функций, которые помогут нам нарисовать закругленные треугольники, которые мы будем использовать в качестве стрелок.
//+------------------------------------------------------------------+ //| Fill rounded triangle | //+------------------------------------------------------------------+ void FillRoundedTriangle(CCanvas &cvs, double &sharpX[], double &sharpY[], int radius, uint fillColor, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) { double minY = sharpY[0], maxY = sharpY[0]; for(int i = 1; i < 3; i++) { if(sharpY[i] < minY) minY = sharpY[i]; if(sharpY[i] > maxY) maxY = sharpY[i]; } int yStart = (int)MathCeil(minY); int yEnd = (int)MathFloor(maxY); for(int y = yStart; y <= yEnd; y++) { double scanlineY = (double)y + 0.5; double xIntersections[12]; int intersectionCount = 0; for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { int nextIndex = (edgeIndex + 1) % 3; double startX = tangentX[edgeIndex][0], startY = tangentY[edgeIndex][0]; double endX = tangentX[nextIndex][1], endY = tangentY[nextIndex][1]; double edgeMinY = (startY < endY) ? startY : endY; double edgeMaxY = (startY > endY) ? startY : endY; if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; if(MathAbs(endY - startY) < 1e-12) continue; double interpolationFactor = (scanlineY - startY) / (endY - startY); if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); } for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { double centerX = arcCentersX[cornerIndex], centerY = arcCentersY[cornerIndex]; double deltaY = scanlineY - centerY; if(MathAbs(deltaY) > radius) continue; double deltaX = MathSqrt(radius*radius - deltaY*deltaY); double candidates[2]; candidates[0] = centerX - deltaX; candidates[1] = centerX + deltaX; for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); if(TriangleAngleInArcSweep(startAngles[cornerIndex], endAngles[cornerIndex], angle)) xIntersections[intersectionCount++] = candidates[candidateIndex]; } } for(int a = 0; a < intersectionCount - 1; a++) for(int b = a + 1; b < intersectionCount; b++) if(xIntersections[a] > xIntersections[b]) { double temp = xIntersections[a]; xIntersections[a] = xIntersections[b]; xIntersections[b] = temp; } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { int xLeft = (int)MathCeil(xIntersections[pairIndex]); int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); for(int x = xLeft; x <= xRight; x++) cvs.PixelSet(x, y, fillColor); } } } //+------------------------------------------------------------------+ //| Draw rounded triangle arrow | //+------------------------------------------------------------------+ void DrawRoundedTriangleArrow(CCanvas &cvs, int baseX, int baseY, int tri_base_width, int tri_height, bool isUp, uint fillColor) { int radius = TriangleRoundRadius * supersamplingFactor; double sharpX[3], sharpY[3]; if (isUp) { sharpX[0] = baseX; sharpY[0] = baseY; sharpX[1] = baseX - tri_base_width / 2.0; sharpY[1] = baseY + tri_height; sharpX[2] = baseX + tri_base_width / 2.0; sharpY[2] = baseY + tri_height; } else { sharpX[0] = baseX; sharpY[0] = baseY + tri_height; sharpX[1] = baseX + tri_base_width / 2.0; // Swapped for consistent winding sharpY[1] = baseY; sharpX[2] = baseX - tri_base_width / 2.0; sharpY[2] = baseY; } double arcCentersX[3], arcCentersY[3]; double tangentX[3][2], tangentY[3][2]; double startAngles[3], endAngles[3]; PrecomputeTriangleGeometry(sharpX, sharpY, radius, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles); FillRoundedTriangle(cvs, sharpX, sharpY, radius, fillColor, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles); }
Мы реализуем функцию "FillRoundedTriangle" для визуализации заполненных закругленных треугольников с использованием подхода, основанного на сканировании строк для точного векторного заполнения, сначала определяя минимальные и максимальные координаты Y от острых вершин "sharpY", чтобы определить вертикальные границы, затем выполняя перебор каждого целого числа y от вершины minY до минимума maxY со смещением на половину пикселя "scanlineY" для лучшего сглаживания за счет субпиксельной точности. Для каждой сканируемой строки мы вычисляем до 12 x-пересечений: сначала для трех прямых касательных ребер путем линейной интерполяции между точками касания "tangentX" и "tangentY", если сканируемая строка попадает в диапазон Y ребра, а затем для каждого из трех углов, проверяя, пересекает ли сканируемая строка дугу решая для deltaX из уравнения окружности в центре дуги "arcCentersX" и "arcCentersY" с заданным радиусом, добавляя возможные значения x только в том случае, если их углы проходят проверку "TriangleAngleInArcSweep", чтобы убедиться, что они лежат в пределах размаха дуги, определенного "startAngles" и "endAngles".
Мы сортируем накопленные x-пересечения в порядке возрастания, используя простую пузырьковую сортировку для повышения эффективности работы с небольшими массивами, затем заполняем горизонтальные промежутки между каждой парой пересечений, устанавливая пиксели от вершины слева по оси x до минимума справа по оси x с помощью предусмотренного "fillColor", что приводит к получению сглаженного, не содержащего артефакта, закругленного треугольника, который использует предварительно вычисленную геометрию для высококачественного рендеринга элементов пользовательского интерфейса, таких как стрелки. Этот метод сканирования строк важен тем, что он позволяет с пиксельной точностью заполнять сложные криволинейные формы, не полагаясь на аппроксимацию, обеспечивая сглаживание краев за счет точного определения пересечений границ, что имеет решающее значение для сохранения четкости изображения в условиях масштабирования или высокого разрешения, когда традиционное заполнение полигонов может привести к появлению «зубцов» или разрывов.
Мы создаем функцию "DrawRoundedTriangleArrow", чтобы рисовать закругленные треугольные стрелки вверх или вниз для наших кнопок прокрутки, масштабируем "TriangleRoundRadius" с помощью "supersamplingFactor" в соответствии с рендерингом в высоком разрешении, определяем три острые вершины "sharpX" и "sharpY" на основе положения основания, ширину основания треугольника "tri_base_width", высоту "tri_height" и направление "isUp" — для стрелок "вверх", располагая точку вверху, а основание - внизу, а для стрелок "вниз" - инвертирование с заменой базовых точек для обеспечения единообразного порядка обхода, чтобы избежать искажений заполнения. Затем мы вызываем функцию "PrecomputeTriangleGeometry" для вычисления центров дуг, касательных и углов относительно этих вершин и радиуса, сохраняя их в массивах "arcCentersX", "arcCentersY", "tangentX", "tangentY", "startAngles" и "endAngles", после чего вызываем функцию "FillRoundedTriangle" с этими параметрами и значением "fillColor" для фактического рендеринга, что позволяет получить плавные, закругленные стрелки, которые органично вписываются в полосу прокрутки, улучшая эстетику и удобство использования пользовательского интерфейса. Мы выполнили повышение дискретизации; пришло время выполнить даунсэмплинг или понижение дискретизации для целевого, финального рендеринга объекта canvas.
//+------------------------------------------------------------------+ //| Downsample canvas (average for AA) | //+------------------------------------------------------------------+ void DownsampleCanvas(CCanvas &targetCanvas, CCanvas &highResCanvas) { int targetWidth = targetCanvas.Width(); int targetHeight = targetCanvas.Height(); for(int pixelY = 0; pixelY < targetHeight; pixelY++) { for(int pixelX = 0; pixelX < targetWidth; pixelX++) { double sourceX = pixelX * supersamplingFactor; double sourceY = pixelY * supersamplingFactor; double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0; double weightSum = 0; for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) { for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) { int sourcePixelX = (int)(sourceX + deltaX); int sourcePixelY = (int)(sourceY + deltaY); if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) { uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY); uchar alpha = (uchar)((pixelValue >> 24) & 0xFF); uchar red = (uchar)((pixelValue >> 16) & 0xFF); uchar green = (uchar)((pixelValue >> 8) & 0xFF); uchar blue = (uchar)(pixelValue & 0xFF); double weight = 1.0; sumAlpha += alpha * weight; sumRed += red * weight; sumGreen += green * weight; sumBlue += blue * weight; weightSum += weight; } } } if(weightSum > 0) { uchar finalAlpha = (uchar)(sumAlpha / weightSum); uchar finalRed = (uchar)(sumRed / weightSum); uchar finalGreen = (uchar)(sumGreen / weightSum); uchar finalBlue = (uchar)(sumBlue / weightSum); uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) | ((uint)finalGreen << 8) | (uint)finalBlue; targetCanvas.PixelSet(pixelX, pixelY, finalColor); } } } }
Мы определяем функцию "DownsampleCanvas" для выполнения сглаживания путем понижения высокого разрешения canvas до целевого размера путем усреднения по пикселям, извлекая целевые размеры из "targetCanvas.Width()" и "targetCanvas.Height()", затем выполняем перебор каждого целевого пикселя с помощью вложенных циклов для "pixelY" и "pixelX". Для каждого целевого пикселя мы сопоставляем его с исходной областью, умножая координаты на "supersamplingFactor", чтобы получить "sourceX" и "sourceY", инициализируя суммы для альфа-канала, красного, зеленого, синего цветов и счетчика "weightSum", после чего выполняем вложенные циклы по "deltaY" и "deltaX" от 0 до "supersamplingFactor" - 1 и отбираем соответствующие пиксели высокого разрешения.
Мы рассчитываем позиции исходных пикселей, проверяем границы внутри "highResCanvas.Width()" и "highResCanvas.Height()", извлекаем компоненты ARGB с помощью PixelGet и битовых сдвигов, добавляем взвешенные вклады (с равномерным весом 1.0) к суммам, и если значение weightSum положительно, усредняем каждый компонент для вычисления окончательных значений uchar, объединяем их в finalColor со сдвигами и устанавливаем его на целевом изображении с помощью метода PixelSet. Этот процесс понижения дискретизации необходим для получения плавных визуальных эффектов со сглаживанием за счет смешивания нескольких изображений высокого разрешения на целевой пиксель, уменьшения «рваных» краев в визуализируемых элементах, таких как границы и фигуры, и, таким образом, повышения общего качества графики на панели без больших вычислительных затрат. Теперь у нас есть все необходимые вспомогательные функции, так что мы можем приступать к работе. Начнём с обновления canvas для статистики, который теперь включает в себя рисование на большом canvas с высоким разрешением, а затем уменьшим его до обычного размера.
//+------------------------------------------------------------------+ //| Update stats on canvas | //+------------------------------------------------------------------+ void UpdateStatsOnCanvas() { // Render stats elements canvasStatsHighRes.Erase(0); //--- Clear high-res stats canvas int statsWidthHighRes = (currentWidth / 2) * supersamplingFactor; //--- Scaled width int heightHighRes = currentHeight * supersamplingFactor; //--- Scaled height if (UseBackground && ArraySize(bg_pixels_stats) == (currentWidth / 2) * currentHeight) { //--- Check background valid uint bg_pixels_stats_high[]; //--- High-res bg ArrayResize(bg_pixels_stats_high, statsWidthHighRes * heightHighRes); //--- Resize ScaleImage(bg_pixels_stats_high, currentWidth / 2, currentHeight, statsWidthHighRes, heightHighRes); //--- Scale bg for (int y = 0; y < heightHighRes; y++) { //--- Loop Y for (int x = 0; x < statsWidthHighRes; x++) { //--- Loop X canvasStatsHighRes.PixelSet(x, y, bg_pixels_stats_high[y * statsWidthHighRes + x]); //--- Set pixel } } } if (StatsBackgroundMode != NoColor) { //--- Check background mode for (int y = 0; y < heightHighRes; y++) { //--- Loop rows double factor = (double)y / (heightHighRes - 1); //--- Compute factor color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color uchar alpha = (uchar)(255 * BackgroundOpacity); //--- Compute alpha uint argbFill = ColorToARGB(currentColor, alpha); //--- Convert to ARGB for (int x = 0; x < statsWidthHighRes; x++) { //--- Loop columns uint currentPixel = canvasStatsHighRes.PixelGet(x, y); //--- Get pixel uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels canvasStatsHighRes.PixelSet(x, y, blendedPixel); //--- Set blended pixel } } } if (StatsBackgroundMode != NoColor) { //--- Check background mode for borders double reduction = BorderOpacityPercentReduction / 100.0; //--- Compute reduction double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity uchar alpha = (uchar)(255 * opacity); //--- Set alpha double darkenReduction = BorderDarkenPercent / 100.0; //--- Compute darken reduction double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Compute darken factor for (int y = 0; y < heightHighRes; y++) { //--- Loop vertical borders double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (heightHighRes - 1); //--- Get factor color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base color color darkColor = DarkenColor(baseColor, darkenFactor); //--- Darken color uint argb = ColorToARGB(darkColor, alpha); //--- Convert to ARGB canvasStatsHighRes.PixelSet(0, y, argb); //--- Set left border pixel canvasStatsHighRes.PixelSet(1, y, argb); //--- Set inner left pixel canvasStatsHighRes.PixelSet(statsWidthHighRes - 1, y, argb); //--- Set right border pixel canvasStatsHighRes.PixelSet(statsWidthHighRes - 2, y, argb); //--- Set inner right pixel } double factorTop = 0.0; //--- Set top factor color baseTop = GetTopColor(); //--- Get top base color darkTop = DarkenColor(baseTop, darkenFactor); //--- Darken top uint argbTop = ColorToARGB(darkTop, alpha); //--- Convert top to ARGB for (int x = 0; x < statsWidthHighRes; x++) { //--- Loop top borders canvasStatsHighRes.PixelSet(x, 0, argbTop); //--- Set top pixel canvasStatsHighRes.PixelSet(x, 1, argbTop); //--- Set inner top pixel } double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base color darkBot = DarkenColor(baseBot, darkenFactor); //--- Darken bottom uint argbBot = ColorToARGB(darkBot, alpha); //--- Convert bottom to ARGB for (int x = 0; x < statsWidthHighRes; x++) { //--- Loop bottom borders canvasStatsHighRes.PixelSet(x, heightHighRes - 1, argbBot); //--- Set bottom pixel canvasStatsHighRes.PixelSet(x, heightHighRes - 2, argbBot); //--- Set inner bottom pixel } } else { //--- Handle no background uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border to ARGB canvasStatsHighRes.Line(0, 0, statsWidthHighRes - 1, 0, argbBorder); //--- Draw top border canvasStatsHighRes.Line(statsWidthHighRes - 1, 0, statsWidthHighRes - 1, heightHighRes - 1, argbBorder); //--- Draw right border canvasStatsHighRes.Line(statsWidthHighRes - 1, heightHighRes - 1, 0, heightHighRes - 1, argbBorder); //--- Draw bottom border canvasStatsHighRes.Line(0, heightHighRes - 1, 0, 0, argbBorder); //--- Draw left border canvasStatsHighRes.Line(1, 1, statsWidthHighRes - 2, 1, argbBorder); //--- Draw inner top canvasStatsHighRes.Line(statsWidthHighRes - 2, 1, statsWidthHighRes - 2, heightHighRes - 2, argbBorder); //--- Draw inner right canvasStatsHighRes.Line(statsWidthHighRes - 2, heightHighRes - 2, 1, heightHighRes - 2, argbBorder); //--- Draw inner bottom canvasStatsHighRes.Line(1, heightHighRes - 2, 1, 1, argbBorder); //--- Draw inner left } color labelColor = GetStatsLabelColor(); //--- Get label color color valueColor = GetStatsValueColor(); //--- Get value color color headerColor = GetStatsHeaderColor(); //--- Get header color int yPos = 20 * supersamplingFactor; //--- Scaled Y position for first header string headerText = "Account Stats"; //--- Set header text int fontSizeHigh = StatsHeaderFontSize * supersamplingFactor; //--- Scaled font canvasStatsHighRes.FontSet("Arial Bold", fontSizeHigh); //--- Set header font int textW = canvasStatsHighRes.TextWidth(headerText); //--- Get text width int textH = canvasStatsHighRes.TextHeight(headerText); //--- Get text height int pad = 5 * supersamplingFactor; //--- Scaled padding int rectX = (statsWidthHighRes - textW) / 2 - pad; //--- Compute rect X int rectY = yPos - pad / 2; //--- Compute rect Y int rectW = textW + 2 * pad; //--- Compute rect width int rectH = textH + pad; //--- Compute rect height uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Compute alpha uint argbHeaderBg = ColorToARGB(GetStatsHeaderColor(), alpha); //--- Convert bg to ARGB color header_border_color = GetStatsHeaderColor(); //--- Significant color uint argbHeaderBorder = ColorToARGB(header_border_color, 255); //--- Convert border to ARGB FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 1 * supersamplingFactor); //--- Draw border int yPosSecond = 120 * supersamplingFactor; //--- Scaled Y for second header headerText = "Current Bar Stats"; //--- Set bar header textW = canvasStatsHighRes.TextWidth(headerText); //--- Get width textH = canvasStatsHighRes.TextHeight(headerText); //--- Get height rectX = (statsWidthHighRes - textW) / 2 - pad; //--- Compute rect X rectY = yPosSecond - pad / 2; //--- Compute rect Y rectW = textW + 2 * pad; //--- Compute rect width rectH = textH + pad; //--- Compute rect height FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 2 * supersamplingFactor); //--- Draw border DownsampleCanvas(canvasStats, canvasStatsHighRes); //--- Downsample canvasStats.FontSet("Arial Bold", StatsHeaderFontSize); //--- Set header font uint argbHeader = ColorToARGB(headerColor, 255); //--- Convert header to ARGB canvasStats.TextOut((currentWidth / 2) / 2, 20, "Account Stats", argbHeader, TA_CENTER); //--- Draw first header text canvasStats.TextOut((currentWidth / 2) / 2, 120, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw second header text canvasStats.FontSet("Arial Bold", StatsFontSize); //--- Set stats font uint argbLabel = ColorToARGB(labelColor, 255); //--- Convert label to ARGB uint argbValue = ColorToARGB(valueColor, 255); //--- Convert value to ARGB int yPosDisplay = 50; // Name at 50 canvasStats.TextOut(10, yPosDisplay, "Name:", argbLabel, TA_LEFT); //--- Draw name label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value yPosDisplay += 20; // Balance at 70 canvasStats.TextOut(10, yPosDisplay, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value yPosDisplay += 20; // Equity at 90 canvasStats.TextOut(10, yPosDisplay, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value yPosDisplay += 30; // Second header at 120, then labels yPosDisplay = 150; // Open at 150 (120 +30) canvasStats.TextOut(10, yPosDisplay, "Open:", argbLabel, TA_LEFT); //--- Draw open label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iOpen(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw open value yPosDisplay += 20; // High at 170 canvasStats.TextOut(10, yPosDisplay, "High:", argbLabel, TA_LEFT); //--- Draw high label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iHigh(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw high value yPosDisplay += 20; // Low at 190 canvasStats.TextOut(10, yPosDisplay, "Low:", argbLabel, TA_LEFT); //--- Draw low label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iLow(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw low value yPosDisplay += 20; // Close at 210 canvasStats.TextOut(10, yPosDisplay, "Close:", argbLabel, TA_LEFT); //--- Draw close label canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iClose(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw close value canvasStats.Update(); //--- Update stats canvas }
На панели статистики мы начинаем с очистки canvas статистики высокого разрешения с помощью метода Erase, установленного на 0, подготавливая его к новой отрисовке и вычисляя масштабированные размеры "statsWidthHighRes" и "heightHighRes", путем умножения исходной ширины статистики (половина "currentWidth") и высоты на "supersamplingFactor" для повышения детализации. Если фон включен с помощью функции "UseBackground", а массив "bg_pixels_stats" соответствует исходному размеру, мы объявляем массив фона высокого разрешения "bg_pixels_stats_high", изменяем его размер в соответствии с масштабированными размерами, масштабируем исходные пиксели с помощью "ScaleImage", которая использует бикубическую интерполяцию для сохранения плавности при увеличении, а затем перебираем в цикле каждый пиксель, чтобы установить его на canvas высокого разрешения с помощью метода PixelSet.
Если параметр "StatsBackgroundMode" не равен "NoColor", мы применяем вертикальный градиент или одноцветный фон, перебирая строки с высоким разрешением, вычисляя коэффициент смешивания на основе позиции по оси Y, определяя текущий цвет с помощью "InterpolateColor", если используется градиентный режим, преобразуя в ARGB с учетом непрозрачности из "BackgroundOpacity", и смешивая каждый пиксель в строке с помощью "BlendPixels" после получения существующего значения с помощью PixelGet для достижения прозрачных оверлеев.
Для рамок в фоновых режимах мы вычисляем уменьшенную непрозрачность и коэффициент затемнения на основе входных данных, таких как "BorderOpacityPercentReduction" и "BorderDarkenPercent", затем для вертикальных рамок затемняем базовый цвет для каждой строки и устанавливаем пиксели ARGB на левом и правом краях, включая внутренние; аналогично, для верхних и нижних горизонтальных границ мы используем фиксированные коэффициенты для затемнения и заполнения целых строк, создавая тонкие, учитывающие градиент рамки без резких линий. В случаях отсутствия фона мы преобразуем цвет рамки в ARGB и рисуем внешние и внутренние линии, используя метод Line для верхней, правой, нижней и левой сторон на canvas высокого разрешения, чтобы обеспечить единообразие.
Мы получаем цвета, заданные в соответствии с темой, с помощью таких функций, как "GetStatsLabelColor", затем для заголовка "Account Stats" масштабируем положение по оси Y "yPos" и размер шрифта до высокого разрешения, устанавливаем жирный шрифт, вычисляем центрированные размеры прямоугольника с масштабированным отступом, заполняем фон с помощью "FillRoundedRectangle», используя низкую непрозрачность ARGB и рисуем его рамку с помощью функции "DrawRoundedRectangleBorderHiRes" с толщиной, равной 1, умноженной на суперсэмплинг; повторяем то же самое для заголовка "Current Bar Stats" с масштабированным значением "yPosSecond" и толщиной рамки, равной 2, для разнообразия.
После рендеринга в высоком разрешении мы вызываем функцию "DownsampleCanvas", чтобы усреднить пиксели до стандартного значения "canvasStats", достигая сглаживания, а затем на стандартном canvas устанавливаем жирный шрифт в исходных размерах для отрисовки центрированного текста заголовка с использованием цветов ARGB. Мы переключаемся на статистический шрифт, преобразуем цвета меток и значений в ARGB и отображаем информационные метки и значения торгового счета (имя, баланс, эквити) в позициях отображения с шагом "yPosDisplay", используя TextOut с выравниванием по левому и правому краю, получая данные через функции AccountInfoString и AccountInfoDouble. Продолжая анализ статистики баров, мы отображаем метки и значения OHLC (открытие, максимум, минимум, закрытие) в позициях "yPosDisplay" с дальнейшим увеличением, получая цены с помощью iOpen, "iHigh", iLow, "iClose" для текущего инструмента и периода, отформатированные в цифры с помощью DoubleToString функции. Наконец, мы вызываем функцию "Update" для "canvasStats", чтобы обновить отображение и убедиться, что все улучшения высокого разрешения преобразуются в плавные, профессиональные визуальные элементы на панели статистики. После компиляции получаем следующий результат.

Сделав это, мы теперь обновим текстовую панель, используя тот же подход, чтобы придать ей современный вид.
//+------------------------------------------------------------------+ //| Update text on canvas | //+------------------------------------------------------------------+ void UpdateTextOnCanvas() { // Render text elements canvasTextHighRes.Erase(0); //--- Clear high-res text canvas int textWidthHighRes = canvasText.Width() * supersamplingFactor; //--- Scaled width int textHeightHighRes = canvasText.Height() * supersamplingFactor; //--- Scaled height color text_bg = is_dark_theme ? text_bg_dark : text_bg_light; //--- Get bg color uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert bg to ARGB canvasTextHighRes.FillRectangle(0, 0, textWidthHighRes - 1, textHeightHighRes - 1, argb_bg); //--- Fill background uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border to ARGB canvasTextHighRes.Line(0, 0, textWidthHighRes - 1, 0, argbBorder); //--- Draw top border canvasTextHighRes.Line(textWidthHighRes - 1, 0, textWidthHighRes - 1, textHeightHighRes - 1, argbBorder); //--- Draw right border canvasTextHighRes.Line(textWidthHighRes - 1, textHeightHighRes - 1, 0, textHeightHighRes - 1, argbBorder); //--- Draw bottom border canvasTextHighRes.Line(0, textHeightHighRes - 1, 0, 0, argbBorder); //--- Draw left border int padding = 10 * supersamplingFactor; //--- Scaled padding int textAreaX = padding; //--- Set area X int textAreaY = 0; //--- Set area Y int textAreaWidth = textWidthHighRes - padding * 2; //--- Compute area width int textAreaHeight = textHeightHighRes; //--- Set area height string font = "Calibri"; //--- Set font int fontSize = TextFontSize * supersamplingFactor; // Scaled font size canvasTextHighRes.FontSet(font, fontSize); //--- Apply font int lineHeight = canvasTextHighRes.TextHeight("A"); //--- Get line height text_adjustedLineHeight = (lineHeight + 3) / supersamplingFactor; //--- Adjust line height (display scale) text_visible_height = textAreaHeight / supersamplingFactor; //--- Visible height (display scale) static string wrappedLines[]; //--- Declare wrapped lines static color wrappedColors[]; //--- Declare wrapped colors static bool wrapped = false; //--- Track wrapped state bool need_scroll = false; //--- Set scroll need int reserved_width = 0; //--- Set reserved width if (!wrapped) { //--- Check not wrapped WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Wrap at display scale wrapped = true; //--- Set wrapped flag } int numLines = ArraySize(wrappedLines); //--- Get line count text_total_height = numLines * text_adjustedLineHeight; //--- Compute total height need_scroll = text_total_height > text_visible_height; //--- Check need scroll if (need_scroll) { //--- Handle scroll needed reserved_width = text_track_width * supersamplingFactor; //--- Reserve scaled width textAreaWidth -= reserved_width; //--- Adjust area width WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Rewrap text numLines = ArraySize(wrappedLines); //--- Update line count text_total_height = numLines * text_adjustedLineHeight; //--- Update total height } text_max_scroll = MathMax(0, text_total_height - text_visible_height) * smoothness_factor; //--- Compute max scroll text_scroll_visible = need_scroll; //--- Set scroll visible text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp scroll pos if (text_scroll_visible) { //--- Check scroll visible int scrollbar_y = 0; //--- Set scrollbar Y int scrollbar_height = textAreaHeight; //--- Set scrollbar height int scroll_area_height = scrollbar_height - 2 * (text_button_size * supersamplingFactor); //--- Scaled area height text_slider_height = TextCalculateSliderHeight(); //--- Compute slider height int scrollbar_x = textWidthHighRes - (text_track_width * supersamplingFactor); //--- Scaled scrollbar X color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color uint argb_leader = ColorToARGB(leader_color, 255); //--- Convert to ARGB canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader int slider_y = scrollbar_y + (text_button_size * supersamplingFactor) + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - (text_slider_height * supersamplingFactor))); //--- Scaled slider Y if (text_scroll_area_hovered) { //--- Check area hovered color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light; //--- Get button bg color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg color up_bg; if (ShowUpDownButtons) { up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg; } else { up_bg = leader_color; // Blend with track, no hover } uint argb_up_bg = ColorToARGB(up_bg, (uchar)255); //--- Convert up bg canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + (text_button_size * supersamplingFactor) - 1, argb_up_bg); //--- Fill up button color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light; //--- Get arrow color color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Get up arrow color uint argb_up_arrow = ColorToARGB(up_arrow, (uchar)255); //--- Convert up arrow // Draw up rounded triangle int arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X double base_width = text_button_size * (TriangleBaseWidthPercent / 100.0) * supersamplingFactor; //--- Compute base width int tri_height = (int)(base_width * (TriangleHeightPercent / 100.0)); //--- Compute height int arrow_y = scrollbar_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y DrawRoundedTriangleArrow(canvasTextHighRes, arrow_x, arrow_y, (int)base_width, tri_height, true, argb_up_arrow); //--- Up arrow int down_y = scrollbar_y + scrollbar_height - (text_button_size * supersamplingFactor); //--- Compute down Y color down_bg; if (ShowUpDownButtons) { down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg; } else { down_bg = leader_color; // Blend with track, no hover } uint argb_down_bg = ColorToARGB(down_bg, (uchar)255); //--- Convert down bg canvasTextHighRes.FillRectangle(scrollbar_x, down_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, down_y + (text_button_size * supersamplingFactor) - 1, argb_down_bg); //--- Fill down button color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Get down arrow color uint argb_down_arrow = ColorToARGB(down_arrow, (uchar)255); //--- Convert down arrow // Draw down rounded triangle int down_arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X int down_arrow_y = down_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y DrawRoundedTriangleArrow(canvasTextHighRes, down_arrow_x, down_arrow_y, (int)base_width, tri_height, false, argb_down_arrow); //--- Down arrow int slider_x = scrollbar_x + (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider X int slider_w = (text_track_width * supersamplingFactor) - 2 * (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider width int cap_radius = slider_w / 2; //--- Compute cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine slider bg uint argb_slider = ColorToARGB(slider_bg, (uchar)255); //--- Convert slider to ARGB canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap canvasTextHighRes.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill slider body } else { //--- Handle thin scrollbar int thin_w = text_scrollbar_thin_width * supersamplingFactor; //--- Scaled thin width int thin_x = scrollbar_x + ((text_track_width * supersamplingFactor) - thin_w) / 2; //--- Compute thin X int cap_radius = thin_w / 2; //--- Compute cap radius color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg uint argb_slider = ColorToARGB(slider_bg_color, (uchar)255); //--- Convert to ARGB canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap canvasTextHighRes.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill thin body } } DownsampleCanvas(canvasText, canvasTextHighRes); //--- Downsample color text_base = is_dark_theme ? text_base_dark : text_base_light; //--- Get base color canvasText.FontSet("Calibri", TextFontSize); //--- Normal font for (int line = 0; line < numLines; line++) { //--- Loop lines string lineText = wrappedLines[line]; //--- Get line text if (StringLen(lineText) == 0) continue; //--- Skip empty color lineColor = wrappedColors[line]; //--- Get line color if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5); //--- Adjust dark color else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7); //--- Adjust light color int line_y = (textAreaY / supersamplingFactor) + line * text_adjustedLineHeight - (text_scroll_pos/smoothness_factor); //--- Compute line Y (display scale) if (line_y + text_adjustedLineHeight < 0 || line_y > text_visible_height) continue; //--- Skip out of view if (IsHeading(lineText) ) { //--- Check heading canvasText.FontSet("Calibri Bold", TextFontSize); //--- Set bold font lineColor = clrDodgerBlue; //--- Set heading color } else canvasText.FontSet("Calibri", TextFontSize); //--- Set normal font uint argbText = ColorToARGB(lineColor, 255); //--- Convert to ARGB canvasText.TextOut(textAreaX / supersamplingFactor, line_y, lineText, argbText, TA_LEFT); //--- Draw line text } canvasText.Update(); //--- Update text canvas }
В функции текстового canvas мы начинаем с очистки текстового canvas высокого разрешения с помощью метода Erase, установленного на 0, и вычисляем масштабированные размеры "textWidthHighRes" и "textHeightHighRes", умножая стандартную ширину и высоту canvas на "supersamplingFactor" для детальной отрисовки. Мы выбираем цвет фона "text_bg" на основе темы "is_dark_theme", преобразуем его в ARGB с прозрачностью из параметра "TextBackgroundOpacityPercent" и заполняем весь прямоугольник высокого разрешения с помощью параметра "FillRectangle". Затем рисуем рамки с помощью метода "Line", используя ARGB из параметра "GetBorderColor" для верхнего, правого, нижнего и левого рёбер.
Мы масштабируем отступы до высокого разрешения с помощью параметра "supersamplingFactor", устанавливаем границы текстовой области "textAreaX", "textAreaY", "textAreaWidth" и "textAreaHeight", меняем шрифт на "Calibri" с масштабированным "fontSize" из "TextFontSize", измеряем высоту строки для "A" и корректируем "text_adjustedLineHeight" обратно до масштаба отображения, деля на "supersamplingFactor". Мы вычисляем "text_visible_height" как высоту масштабированной области, деленную на "supersamplingFactor", используем статические массивы "wrappedLines" и "wrappedColors" с флагом "wrapped" для кэширования переноса текста, вызывая функцию "WrapText", если перенос еще не произошел, с параметрами display-scale (размер и ширина шрифта, деленные на коэффициент), чтобы разбить "text_usage_text" на строки с цветами.
Мы определяем количество строк "numLines" и общую высоту "text_total_height" как количество строк, умноженное на скорректированную высоту, проверяем, нужна ли прокрутка "need_scroll", сравнивая ее с видимой высотой, и если да, резервируем масштабированную ширину "reserved_width" для полосы прокрутки, корректируем ширину области, еще раз переносим текст и обновляем счетчики и высоты. Мы вычисляем "text_max_scroll" как избыточную высоту, умноженную на "smoothness_factor" для более точного управления прокруткой, устанавливаем "text_scroll_visible" в соответствии с флагом need, ограничиваем "text_scroll_pos" значениями от 0 до max, и, если текст виден, располагаем полосу прокрутки справа с масштабированной шириной дорожки, заполняем прямоугольник подложки полосы прокрутки цветом "argb_leader" на основе темы из "leader_color".
Для позиции ползунка "slider_y" мы масштабируем размер кнопки и высоту области, вычисляем высоту с помощью "TextCalculateSliderHeight" (исходный масштаб), и если область находится под курсором "text_scroll_area_hovered", условно устанавливаем фон кнопок вверх и вниз на основе функции "ShowUpDownButtons" — используя цвета при наведении курсора, если они отображаются, в противном случае смешивая с цветом подложки — заполняем их прямоугольники, определяем цвета стрелок с учетом состояний отключения или наведения курсора и рисуем закругленные треугольные стрелки вверх и вниз с помощью функции "DrawRoundedTriangleArrow", используя вычисленную ширину основания из "TriangleBaseWidthPercent", высоту из "TriangleHeightPercent", центрированные позиции и направление "isUp" или false.
Затем мы рисуем ползунок с масштабированным отступом "slider_x", шириной "slider_w" и радиусом закругления, выбирая фон в зависимости от состояния наведения курсора или перемещения "text_scroll_slider_hovered" или "text_movingStateSlider", рисуя верхнюю и нижнюю дуги с помощью "Arc", заполняя закругления с помощью FillCircle, а тело с помощью FillRectangle; для тонкого режима без наведения курсора аналогично рисуем более узкий ползунок, центрированный в дорожке. После рендеринга в высоком разрешении мы понижаем дискретизацию до стандартного "canvasText" с помощью функции "DownsampleCanvas" для получения сглаженного изображения. Мы выбираем цвет текста на основе темы "text_base", устанавливаем шрифт "Calibri" в исходное значение "TextFontSize", проходим циклом по перенесенным строкам, пропуская пустые места, корректируем цвета для темы с помощью "LightenColor" или "DarkenColor", вычисляем положение по оси Y в масштабе отображения "line_y", вычитая сглаженную прокрутку "text_scroll_pos / smoothness_factor", пропускаем строки, выходящие за пределы видимой области, выделяем заголовки жирным шрифтом "Calibri Bold" и устанавливаем значение "clrDodgerBlue", преобразуем в ARGB и отображаем каждый из них с помощью TextOut в масштабированных значениях X и Y. Наконец, мы вызываем метод Update для объекта "canvasText", чтобы отобразить улучшенную, плавную текстовую панель с улучшенными прокруткой и визуальными элементами. После компиляции получаем следующий результат.

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

Заключение
В заключение отметим, что мы улучшили панель canvas на MQL5 с помощью методов сглаживания и рендеринга с высоким разрешением, используя суперсэмплинг для получения более плавной графики и элементов. Мы добавили объекты canvase с высоким разрешением для статистических и текстовых панелей, а также точные функции рисования закругленных рамок, прямоугольников и стрелок, улучшив общее качество изображения. Благодаря улучшенному сглаживанию и рендерингу в высоком разрешении вы сможете создавать более четкие и профессиональные торговые панели, готовые к дальнейшим пользовательским настройкам в процессе разработки. Удачной торговли!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21252
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Особенности написания Пользовательских Индикаторов
Разработка инструментария для анализа движения цен (Часть 24): Инструмент количественного анализа Price Action
Разработка инструментария для анализа движения цен (Часть 26): Инструмент для работы с несколькими паттернами – пин-баром, паттернами поглощения и дивергенцией RSI
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования