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

Реализация средствами MQL5
Чтобы создать программу на MQL5, откройте MetaEditor, перейдите в Навигатор, найдите папку «Советники» (Experts), щелкните кнопкой мыши на вкладке "Создать" (New) и следуйте инструкциям по созданию файла. Как только это будет сделано, в среде программирования нужно будет объявить некоторые входные параметры и глобальные переменные, которые будем использовать во всей программе.
//+------------------------------------------------------------------+ //| Rounded Rectangle & Triangle PART1.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 #include <Canvas\Canvas.mqh> //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ input group "Position" input int shapesPositionX = 20; // Shapes X position input int shapesPositionY = 20; // Shapes Y position input int shapesGapPixels = 15; // Gap between shapes (pixels) input group "Rectangle" input int rectangleWidthPixels = 250; // Rectangle width input int rectangleHeightPixels = 100; // Rectangle height input int rectangleCornerRadiusPixels = 5; // Rectangle corner radius input bool rectangleShowBorder = true; // Show rectangle border input int rectangleBorderThicknessPixels = 1; // Rectangle border thickness input color rectangleBorderColor = clrBlue; // Rectangle border color input int rectangleBorderOpacityPercent = 80; // Rectangle border opacity (0-100%) input color rectangleBackgroundColor = clrBlue; // Rectangle background color input int rectangleBackgroundOpacityPercent= 30; // Rectangle background opacity (0-100%) input group "Triangle" input int triangleBaseWidthPixels = 250; // Triangle base width (pixels) input double triangleHeightAsPercentOfWidth = 86.6; // Height as % of width (86.6=equilateral, <86.6=flat, >86.6=tall) input int triangleCornerRadiusPixels = 12; // Triangle corner radius input bool triangleShowBorder = true; // Show triangle border input int triangleBorderThicknessPixels = 1; // Triangle border thickness input color triangleBorderColor = clrRed; // Triangle border color input int triangleBorderOpacityPercent = 80; // Triangle border opacity (0-100%) input color triangleBackgroundColor = clrRed; // Triangle background color input int triangleBackgroundOpacityPercent = 30; // Triangle background opacity (0-100%) input group "General" input int supersamplingLevel = 4; // Supersampling level (1=off, 2=2x, 4=4x) //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ CCanvas rectangleCanvas, rectangleHighResCanvas; //--- Declare rectangle canvas objects CCanvas triangleCanvas, triangleHighResCanvas; //--- Declare triangle canvas objects string rectangleCanvasName = "RoundedRectCanvas"; //--- Set rectangle canvas name string triangleCanvasName = "RoundedTriCanvas"; //--- Set triangle canvas name int supersamplingFactor; //--- Store supersampling factor int computedTriangleHeightPixels; //--- Store computed triangle height in pixels double triangleSharpVerticesX[3], triangleSharpVerticesY[3]; //--- Store sharp vertices for triangle double triangleArcCentersX[3], triangleArcCentersY[3]; //--- Store arc centers for triangle double triangleTangentPointsX[3][2], triangleTangentPointsY[3][2]; //--- Store tangent points for triangle double triangleArcStartAngles[3], triangleArcEndAngles[3]; //--- Store arc sweep angles in radians int triangleHighResWidth, triangleHighResHeight; //--- Store high-res dimensions for triangle
Начнем реализацию с подключения библиотеки Canvas с помощью макроса "#include <Canvas\Canvas.mqh>", который предоставляет необходимые классы и методы для создания и управления графическими объектами canvas в MQL5, позволяя нам рисовать пользовательские фигуры, такие как прямоугольники и треугольники, непосредственно на графике.
Далее мы организуем пользовательские данные в логические группы для более удобной настройки: группа "Position" (Положение) с параметрами для координат X и Y фигур и промежуток между ними; группа "Rectangle" (Прямоугольник), определяющая ширину, высоту, радиус скругления углов, параметры рамки, включая видимость, толщину, цвет и прозрачность, а также цвет и прозрачность фона; группа "Triangle" (Треугольник), аналогично задающая ширину основания, высоту в процентах от ширины (по умолчанию 86,6 для равносторонних пропорций), радиус скругления углов и соответствующие настройки рамки и фона; а также группа "General" (Общая) с уровнем суперсэмплирования для управления качеством сглаживания изображения (1 — отсутствие сглаживания, более высокие значения, например, 4, — означают улучшенную плавность).
Для поддержки рендеринга мы объявляем глобальные объекты canvas как для стандартной версии прямоугольника и треугольника, так и для версии с высоким разрешением, присваивая им имена типа "RoundedRectCanvas" и "RoundedTriCanvas" для идентификации. Наконец, мы создадим переменные для хранения коэффициента суперсэмплирования, вычисленной высоты треугольника и массивов для геометрии треугольника, включая острые вершины, центры дуг, точки касания (в виде массивов 3x2 для каждого угла), начальные и конечные углы в радианах, а также размеры в высоком разрешении для canvas треугольника. После установки глобальных переменных нам потребуется объявить несколько вспомогательных функций, которые будут использоваться на протяжении всей программы.
//+------------------------------------------------------------------+ //| Shared Utilities | //+------------------------------------------------------------------+ uint ColorToARGBWithOpacity(color clr, int opacityPercent) { uchar redComponent = (uchar)((clr >> 0) & 0xFF); //--- Extract red component uchar greenComponent = (uchar)((clr >> 8) & 0xFF); //--- Extract green component uchar blueComponent = (uchar)((clr >> 16) & 0xFF); //--- Extract blue component uchar alphaComponent = (uchar)((opacityPercent * 255) / 100); //--- Calculate alpha component from opacity return ((uint)alphaComponent << 24) | ((uint)redComponent << 16) | ((uint)greenComponent << 8) | (uint)blueComponent; //--- Combine into ARGB value } void BicubicDownsample(CCanvas &targetCanvas, CCanvas &highResCanvas) { int targetWidth = targetCanvas.Width(); //--- Get target canvas width int targetHeight = targetCanvas.Height(); //--- Get target canvas height for(int pixelY = 0; pixelY < targetHeight; pixelY++) { //--- Loop over target height pixels for(int pixelX = 0; pixelX < targetWidth; pixelX++) { //--- Loop over target width pixels double sourceX = pixelX * supersamplingFactor; //--- Calculate source X position double sourceY = pixelY * supersamplingFactor; //--- Calculate source Y position double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0; //--- Initialize sum variables double weightSum = 0; //--- Initialize weight sum for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) { //--- Loop over delta Y for supersampling for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) { //--- Loop over delta X for supersampling int sourcePixelX = (int)(sourceX + deltaX); //--- Compute source pixel X int sourcePixelY = (int)(sourceY + deltaY); //--- Compute source pixel Y if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) { //--- Check if within high-res bounds uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY); //--- Get pixel value from high-res canvas uchar alpha = (uchar)((pixelValue >> 24) & 0xFF); //--- Extract alpha component uchar red = (uchar)((pixelValue >> 16) & 0xFF); //--- Extract red component uchar green = (uchar)((pixelValue >> 8) & 0xFF); //--- Extract green component uchar blue = (uchar)(pixelValue & 0xFF); //--- Extract blue component double weight = 1.0; //--- Set weight to 1.0 sumAlpha += alpha * weight; //--- Accumulate weighted alpha sumRed += red * weight; //--- Accumulate weighted red sumGreen += green * weight; //--- Accumulate weighted green sumBlue += blue * weight; //--- Accumulate weighted blue weightSum += weight; //--- Accumulate total weight } } } if(weightSum > 0) { //--- Check if weight sum is positive uchar finalAlpha = (uchar)(sumAlpha / weightSum); //--- Compute final alpha uchar finalRed = (uchar)(sumRed / weightSum); //--- Compute final red uchar finalGreen = (uchar)(sumGreen / weightSum); //--- Compute final green uchar finalBlue = (uchar)(sumBlue / weightSum); //--- Compute final blue uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) | ((uint)finalGreen << 8) | (uint)finalBlue; //--- Combine into final color targetCanvas.PixelSet(pixelX, pixelY, finalColor); //--- Set pixel on target canvas } } } } double NormalizeAngle(double angle) { double twoPi = 2.0 * M_PI; //--- Define two pi constant angle = MathMod(angle, twoPi); //--- Modulo angle by two pi if(angle < 0) angle += twoPi; //--- Adjust if angle is negative return angle; //--- Return normalized angle } bool IsAngleBetween(double angle, double startAngle, double endAngle) { angle = NormalizeAngle(angle); //--- Normalize angle startAngle = NormalizeAngle(startAngle); //--- Normalize start angle endAngle = NormalizeAngle(endAngle); //--- Normalize end angle double span = NormalizeAngle(endAngle - startAngle); //--- Compute span double relativeAngle = NormalizeAngle(angle - startAngle); //--- Compute relative angle return relativeAngle <= span; //--- Return if within span }
Начнем с создания функции "ColorToARGBWithOpacity", которая преобразует цвет в формат ARGB, добавляя при этом заданный процент непрозрачности. Мы извлекаем красную, зеленую и синюю составляющие с помощью битовых сдвигов, вычисляем альфа-канал, масштабируя непрозрачность в диапазоне 0-255, и объединяем их в одно целочисленное значение, что позволяет создавать прозрачные заливки и рамок для наших фигур. Далее мы реализуем функцию "BicubicDownsample" для выполнения сглаживания изображения при понижении дискретизации от высокого разрешения объекта canvas до целевого значения разрешения. Мы получаем целевые размеры, перебираем каждый пиксель, сопоставляем его с областью исходного изображения, полученного методом суперсэмплирования, накапливаем взвешенные суммы компонентов ARGB из субпикселей (с равномерным весом для усреднения) и, если существуют образцы, вычисляем окончательные значения перед установкой пикселя, тем самым сглаживая края путем смешивания деталей из более высокого разрешения.
Для обеспечения согласованности угловых вычислений мы определяем функцию "NormalizeAngle", используя константу, равную двум пи, для вычисления остатка от деления угла и корректировки отрицательных значений, гарантируя, что все углы находятся в диапазоне от 0 до 2 пи для надежного сравнения при рендеринге дуг. Далее мы добавляем функцию "IsAngleBetween", которая проверяет, находится ли угол в пределах начального и конечного диапазонов, нормализует входные данные, вычисляет нормализованный диапазон и относительное положение и возвращает значение true, если он находится в заданном диапазоне. Это крайне важно для точного включения пикселей в изогнутые рамки без перекрытия или разрывов. В дополнение к этим угловым операциям нам также понадобится функция для заполнения четырехугольной формы.
void FillQuadrilateral(CCanvas &canvas, double &verticesX[], double &verticesY[], uint fillColor) { double minY = verticesY[0], maxY = verticesY[0]; //--- Initialize min and max Y for(int i = 1; i < 4; i++) { //--- Loop over vertices if(verticesY[i] < minY) minY = verticesY[i]; //--- Update min Y if(verticesY[i] > maxY) maxY = verticesY[i]; //--- Update max Y } int yStart = (int)MathCeil(minY); //--- Compute start Y int yEnd = (int)MathFloor(maxY); //--- Compute end Y for(int y = yStart; y <= yEnd; y++) { //--- Loop over scanlines double scanlineY = (double)y + 0.5; //--- Set scanline Y position double xIntersections[8]; //--- Declare intersections array int intersectionCount = 0; //--- Initialize intersection count for(int i = 0; i < 4; i++) { //--- Loop over edges int nextIndex = (i + 1) % 4; //--- Get next index double x0 = verticesX[i], y0 = verticesY[i]; //--- Get start coordinates double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex]; //--- Get end coordinates double edgeMinY = (y0 < y1) ? y0 : y1; //--- Compute edge min Y double edgeMaxY = (y0 > y1) ? y0 : y1; //--- Compute edge max Y if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y range if(MathAbs(y1 - y0) < 1e-12) continue; //--- Skip if horizontal edge double interpolationFactor = (scanlineY - y0) / (y1 - y0); //--- Compute interpolation factor if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment xIntersections[intersectionCount++] = x0 + interpolationFactor * (x1 - x0); //--- Add intersection X } for(int a = 0; a < intersectionCount - 1; a++) //--- Sort intersections (bubble sort) for(int b = a + 1; b < intersectionCount; b++) //--- Inner loop for sorting if(xIntersections[a] > xIntersections[b]) { //--- Check if swap needed double temp = xIntersections[a]; //--- Temporary store xIntersections[a] = xIntersections[b]; //--- Swap values xIntersections[b] = temp; //--- Complete swap } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X for(int x = xLeft; x <= xRight; x++) //--- Loop over horizontal span canvas.PixelSet(x, y, fillColor); //--- Set pixel with fill color } } }
Мы реализуем функцию "FillQuadrilateral" для визуализации заполненных четырехугольников на canvas с использованием алгоритма сканирования строк, который обеспечивает точное векторное заполнение таких фигур, как рамки или тела, не полагаясь на встроенные методы, которые могут не поддаваться контролю. Для этого мы сначала определяем вертикальные границы, находя минимальные и максимальные координаты Y из входных вершин "verticesY", а затем вычисляем начальные и конечные целочисленные значения сканирования строк, используя значения ceiling и floor для полного покрытия. Для каждой строки сканирования y мы смещаем на полпикселя "scanlineY" для достижения субпиксельной точности, что способствует сглаживанию, и собираем до 8 точек пересечения по оси x путем интерполяции вдоль каждого из четырех ребер (используя остаток от деления для циклического закрытия), если строка сканирования пересекает ребро вертикально, пропуская горизонтальные точки или точки, выходящие за пределы диапазона.
Для сортировки этих пересечений мы используем пузырьковую сортировку для небольших массивов, а затем заполняем горизонтальные промежутки между парами значений x, задавая пиксели от верхнего левого угла до нижнего правого с помощью параметра "fillColor", используя метод PixelSet. Этот метод имеет решающее значение для обработки невыпуклых или неправильных четырехугольников при рендеринге высокого разрешения, поскольку он вычисляет точную заливку пиксель за пикселем, обеспечивая плавные границы в округлых формах за счет заполнения утолщенных реберных полос без наложений или зазоров.
Если вам интересно, что это за алгоритм сканирования строк, мы немного объясним, что это такое, чтобы у вас было понимание. Этот алгоритм обрабатывает изображение слева направо, сканируя по одной горизонтальной строке за раз, а не обрабатывая отдельные пиксели. Он фиксирует все точки пересечения ребер вдоль каждой строки сканирования и заполняет многоугольник, раскрашивая области между парами пересечений.
Это можно сравнить с проведением прямой линии поперек фигуры на бумаге одной ручкой: начиная от левой границы и двигаясь вправо, вы рисуете непрерывно, но всякий раз, когда сталкиваетесь с пересечением с границей многоугольника, вы соответственно останавливаете или возобновляете рисование. Алгоритм работает по тому же принципу. На рисунке ниже проиллюстрировано это поведение: красные точки представляют вершины многоугольника, в то время как синие точки указывают точки пересечения вдоль линии сканирования.

После этого мы можем использовать эти функции для создания округлых форм. Начнем с прямоугольника. Для этого нам также понадобятся вспомогательные функции, чтобы сделать наш код модульным.
void FillRoundedRectangleHiRes(int positionX, int positionY, int width, int height, int radius, uint fillColor) { rectangleHighResCanvas.FillRectangle(positionX + radius, positionY, positionX + width - radius, positionY + height, fillColor); //--- Fill central rectangle rectangleHighResCanvas.FillRectangle(positionX, positionY + radius, positionX + radius, positionY + height - radius, fillColor); //--- Fill left strip rectangleHighResCanvas.FillRectangle(positionX + width - radius, positionY + radius, positionX + width, positionY + height - radius, fillColor); //--- Fill right strip FillCircleQuadrant(positionX + radius, positionY + radius, radius, fillColor, 2); //--- Fill top-left quadrant FillCircleQuadrant(positionX + width - radius, positionY + radius, radius, fillColor, 1); //--- Fill top-right quadrant FillCircleQuadrant(positionX + radius, positionY + height - radius, radius, fillColor, 3); //--- Fill bottom-left quadrant FillCircleQuadrant(positionX + width - radius, positionY + height - radius, radius, fillColor, 4); //--- Fill bottom-right quadrant } void FillCircleQuadrant(int centerX, int centerY, int radius, uint fillColor, int quadrant) { double radiusDouble = (double)radius; //--- Convert radius to double for(int deltaY = -radius - 1; deltaY <= radius + 1; deltaY++) { //--- Loop over delta Y for(int deltaX = -radius - 1; deltaX <= radius + 1; deltaX++) { //--- Loop over delta X bool inQuadrant = false; //--- Initialize quadrant flag if(quadrant == 1 && deltaX >= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-right else if(quadrant == 2 && deltaX <= 0 && deltaY <= 0) inQuadrant = true; //--- Check top-left else if(quadrant == 3 && deltaX <= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-left else if(quadrant == 4 && deltaX >= 0 && deltaY >= 0) inQuadrant = true; //--- Check bottom-right if(!inQuadrant) continue; //--- Skip if not in quadrant double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance if(distance <= radiusDouble) //--- Check if within radius rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel } } } void DrawRoundedRectangleBorderHiRes(int positionX, int positionY, int width, int height, int radius, uint borderColorARGB) { int scaledThickness = rectangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness DrawRectStraightEdge(positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB); //--- Draw top edge DrawRectStraightEdge(positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB); //--- Draw bottom edge DrawRectStraightEdge(positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB); //--- Draw left edge DrawRectStraightEdge(positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB); //--- Draw right edge DrawRectCornerArcPrecise(positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc DrawRectCornerArcPrecise(positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc DrawRectCornerArcPrecise(positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc DrawRectCornerArcPrecise(positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc } void DrawRectStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) { double deltaX = endX - startX; //--- Compute delta X double deltaY = endY - startY; //--- Compute delta Y double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length if(edgeLength < 1e-6) return; //--- Return if length too small double perpendicularX = -deltaY / edgeLength; //--- Compute perpendicular X double perpendicularY = deltaX / edgeLength; //--- Compute perpendicular Y double edgeDirectionX = deltaX / edgeLength; //--- Compute edge direction X double edgeDirectionY = deltaY / edgeLength; //--- Compute edge direction Y double halfThickness = (double)thickness / 2.0; //--- Compute half thickness double extensionLength = 1.5; //--- Set extension length double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y double verticesX[4], verticesY[4]; //--- Declare vertices arrays verticesX[0] = extendedStartX - perpendicularX * halfThickness; verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0 verticesX[1] = extendedStartX + perpendicularX * halfThickness; verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1 verticesX[2] = extendedEndX + perpendicularX * halfThickness; verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2 verticesX[3] = extendedEndX - perpendicularX * halfThickness; verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3 FillQuadrilateral(rectangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge } void DrawRectCornerArcPrecise(int centerX, int centerY, int radius, int thickness, uint borderColor, double startAngle, double endAngle) { int halfThickness = thickness / 2; //--- Compute half thickness double outerRadius = (double)radius + halfThickness; //--- Compute outer radius double innerRadius = (double)radius - halfThickness; //--- Compute inner radius if(innerRadius < 0) innerRadius = 0; //--- Set inner radius to zero if negative int pixelRange = (int)(outerRadius + 2); //--- Compute pixel range for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY); //--- Compute distance if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle if(IsAngleBetween(angle, startAngle, endAngle)) //--- Check if angle within range rectangleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, borderColor); //--- Set pixel } } }
Начнём с реализации функции "FillRoundedRectangleHiRes", которая отобразит закрашенное тело скруглённого прямоугольника на объекте canvas высокого разрешения. Сначала заполняем центральную прямоугольную область, исключая углы. Далее, слева и справа добавляем вертикальные полосы, чтобы соединить прямые стороны. Такой подход обеспечивает бесшовное покрытие без наложений. Для завершения закругленных углов вызываем функцию "FillCircleQuadrant" для каждого квадранта. Мы передаем соответствующие значения центра, радиуса, цвета заливки и номер квадранта (1 для верхнего правого угла, 2 для верхнего левого угла и т. д.). Эта функция перебирает немного увеличенный диапазон пикселей, проверяет, находятся ли точки внутри квадранта и радиуса с помощью вычисления расстояния, и устанавливает подходящие пиксели. Это обеспечивает точное заполнение в виде четверти круга, плавно переходящее в полосы.
Далее мы создаём функцию "DrawRoundedRectangleBorderHiRes" для обработки границ, масштабирования толщины с помощью суперсэмплинга, отрисовки четырёх прямых граней с помощью функции "DrawRectStraightEdge" для верхней, нижней, левой и правой сторон, а также рендеринга угловых дуг с помощью функции "DrawRectCornerArcPrecise" с использованием предопределённых углов в радианах (например, от pi до 1,5pi для верхнего левого угла), что обеспечивает согласованную кривизну и сглаживание граней. В функции "DrawRectStraightEdge" мы вычисляем направления векторов и перпендикуляры от начальной до конечной точки, немного удлиняем линию для лучшего соединения углов, определяем четырехстороннюю полосу со смещениями в половину толщины и заливаем ее с помощью ранее определенной функции quadrilateral, создавая толстые, гладкие, прямые рамки, идеально совпадающие с дугами.
Наконец, функция "DrawRectCornerArcPrecise" формирует кольцо между внутренним и внешним радиусами, перебирая пиксели, проверяя расстояния и углы с помощью функции "IsAngleBetween" и устанавливая пиксели цвета рамки только в пределах указанной дуги, что крайне важно для получения высококачественных, криволинейных рамок без зазубрин в масштабированных изображениях. Теперь можно использовать эти функции для рисования скругленного прямоугольника.
//+------------------------------------------------------------------+ //| Rounded Rectangle | //+------------------------------------------------------------------+ void DrawRoundedRectangle() { int positionX = 10 * supersamplingFactor; //--- Set X position scaled int positionY = 10 * supersamplingFactor; //--- Set Y position scaled int scaledWidth = rectangleWidthPixels * supersamplingFactor; //--- Scale width int scaledHeight = rectangleHeightPixels * supersamplingFactor; //--- Scale height int scaledRadius = rectangleCornerRadiusPixels * supersamplingFactor; //--- Scale radius uint backgroundColorARGB = ColorToARGBWithOpacity(rectangleBackgroundColor, rectangleBackgroundOpacityPercent); //--- Get background ARGB uint borderColorARGB = ColorToARGBWithOpacity(rectangleBorderColor, rectangleBorderOpacityPercent); //--- Get border ARGB FillRoundedRectangleHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, backgroundColorARGB); //--- Fill high-res rectangle if(rectangleShowBorder && rectangleBorderThicknessPixels > 0) //--- Check if border should be shown DrawRoundedRectangleBorderHiRes(positionX, positionY, scaledWidth, scaledHeight, scaledRadius, borderColorARGB); //--- Draw border on high-res BicubicDownsample(rectangleCanvas, rectangleHighResCanvas); //--- Downsample to display canvas rectangleCanvas.FontSet("Arial", 13, FW_NORMAL); //--- Set font for text string displayText = "Rounded Rectangle"; //--- Set display text int textWidth, textHeight; //--- Declare text dimensions rectangleCanvas.TextSize(displayText, textWidth, textHeight); //--- Get text size int textPositionX = 10 + (rectangleWidthPixels - textWidth) / 2; //--- Compute text X position int textPositionY = 10 + (rectangleHeightPixels - textHeight) / 2; //--- Compute text Y position rectangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas }
Здесь мы определяем функцию "DrawRoundedRectangle" для управления отрисовкой скругленного прямоугольника на объекте canvas, начиная с масштабирования смещений положения, ширины, высоты и радиуса с использованием коэффициента суперсэмплирования для обеспечения точности с высоким разрешением и готовности к сглаживанию. Далее мы преобразуем цвета фона и рамок в формат ARGB, добавляя прозрачность с помощью функции "ColorToARGBWithOpacity", которая позволяет создавать полупрозрачные эффекты, усиливающие визуальную глубину без полной прозрачности. Для создания фигуры мы вызываем функцию "FillRoundedRectangleHiRes" с указанными параметрами масштабирования и цветом фона, чтобы заполнить внутреннюю часть объекта canvas высокого разрешения. Если границы включены с помощью функции "rectangleShowBorder" и толщина положительная, вызываем функцию "DrawRoundedRectangleBorderHiRes", чтобы добавить контур с цветом рамки.
Затем понижаем дискретизацию объекта canvas высокого разрешения до стандартной, используя функцию "BicubicDownsample", смешивая детали для получения плавного результата. Наконец, на стандартном объекте canvas мы устанавливаем шрифт с помощью FontSet, используя "Arial" размером 13 и толщиной FW_NORMAL, вычисляем центрированные позиции для метки "Rounded Rectangle" с помощью TextSize, чтобы получить размеры, и рисуем ее с помощью TextOut непрозрачным черным цветом (0xFF000000) с выравниванием по левому краю, создавая дескриптивный оверлей для большей наглядности. Теперь можно вызвать эту функцию в обработчике события инициализации, чтобы отобразить закругленный прямоугольник.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { supersamplingFactor = supersamplingLevel; //--- Assign supersampling factor from input if(supersamplingFactor < 1) { //--- Check if supersampling factor is less than 1 Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message supersamplingFactor = 1; //--- Set supersampling factor to minimum value } int rectangleCanvasWidth = rectangleWidthPixels + 40; //--- Compute rectangle canvas width with padding int rectangleCanvasHeight = rectangleHeightPixels + 40; //--- Compute rectangle canvas height with padding int rectanglePositionY = shapesPositionY; //--- Set rectangle Y position if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY, rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires", rectangleCanvasWidth * supersamplingFactor, rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } rectangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear rectangle canvas rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res rectangle canvas DrawRoundedRectangle(); //--- Draw rounded rectangle rectangleCanvas.Update(); //--- Update rectangle canvas display return(INIT_SUCCEEDED); //--- Return initialization success }
В обработчике OnInit мы инициализируем программу, присваивая введенное пользователем значение "supersamplingLevel" глобальному значению "supersamplingFactor", проверяем, меньше ли оно, чем 1, и сбрасываем его до минимального значения, выводя предупреждение о корректном сглаживании. Далее мы вычисляем размеры прямоугольного объекта canvas, добавляя отступы к ширине и высоте входного значения, устанавливаем его положение по оси Y из значения "shapesPositionY" и создаем стандартный объект canvas с помощью функции CreateBitmapLabel, указывая идентификатор графика, подокно, имя, положение, размер и параметр COLOR_FORMAT_ARGB_NORMALIZE для поддержки прозрачности, выводя сообщения об ошибке с помощью GetLastError и возвращая INIT_FAILED в случае неудачи.
Затем мы создаём canvas высокого разрешения с помощью функции Create, указывая имя с суффиксом, масштабированные размеры, умноженные на "supersamplingFactor", и тот же цветовой формат, обрабатывая ошибки аналогичным образом. Для подготовки к рисованию очищаем оба объекта canvas с помощью функции Erase, передавая прозрачный цвет ARGB из clrNONE. Наконец, вызываем функцию "DrawRoundedRectangle" для выполнения фактического рендеринга, обновляем стандартное отображение объекта canvas с помощью "Update" и возвращаем INIT_SUCCEEDED для подтверждения успешной настройки. После компиляции получаем следующий результат.

На изображении видно, что мы отобразили закругленный прямоугольник. Теперь остается лишь использовать аналогичный подход для отрисовки закругленного треугольника. Для этого мы используем следующую логику. Начнём с создания вспомогательной функции для предварительного вычисления геометрии треугольника.
//+------------------------------------------------------------------+ //| Rounded Triangle | //+------------------------------------------------------------------+ void PrecomputeTriangleGeometry() { int scalingFactor = supersamplingFactor; //--- Set scaling factor double basePositionX = 10.0 * scalingFactor; //--- Set base X position scaled double basePositionY = 10.0 * scalingFactor; //--- Set base Y position scaled double baseWidth = (double)triangleBaseWidthPixels * scalingFactor; //--- Scale base width double baseHeight = (double)computedTriangleHeightPixels * scalingFactor; //--- Scale base height triangleSharpVerticesX[0] = basePositionX + baseWidth / 2.0; triangleSharpVerticesY[0] = basePositionY; //--- Set top vertex triangleSharpVerticesX[1] = basePositionX; triangleSharpVerticesY[1] = basePositionY + baseHeight; //--- Set bottom-left vertex triangleSharpVerticesX[2] = basePositionX + baseWidth; triangleSharpVerticesY[2] = basePositionY + baseHeight; //--- Set bottom-right vertex double scaledRadius = (double)triangleCornerRadiusPixels * scalingFactor; //--- Scale radius for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { //--- Loop over corners int previousIndex = (cornerIndex + 2) % 3; //--- Get previous index int nextIndex = (cornerIndex + 1) % 3; //--- Get next index double edgeA_X = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex], edgeA_Y = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute edge A vector double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y); //--- Compute edge A length edgeA_X /= edgeA_Length; edgeA_Y /= edgeA_Length; //--- Normalize edge A double edgeB_X = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex], edgeB_Y = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute edge B vector double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y); //--- Compute edge B length edgeB_X /= edgeB_Length; edgeB_Y /= edgeB_Length; //--- Normalize edge B double normalA_X = edgeA_Y, normalA_Y = -edgeA_X; //--- Compute normal A double normalB_X = edgeB_Y, normalB_Y = -edgeB_X; //--- Compute normal B double bisectorX = normalA_X + normalB_X, bisectorY = normalA_Y + normalB_Y; //--- Compute bisector double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); //--- Compute bisector length if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); } //--- Handle small bisector bisectorX /= bisectorLength; bisectorY /= bisectorLength; //--- Normalize bisector double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y; //--- Compute cosine of interior angle if(cosInteriorAngle > 1.0) cosInteriorAngle = 1.0; //--- Clamp cosine upper if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0; //--- Clamp cosine lower double halfAngle = MathArccos(cosInteriorAngle) / 2.0; //--- Compute half angle double sinHalfAngle = MathSin(halfAngle); //--- Compute sine of half angle if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12; //--- Set minimum sine value double distanceToCenter = scaledRadius / sinHalfAngle; //--- Compute distance to arc center triangleArcCentersX[cornerIndex] = triangleSharpVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X triangleArcCentersY[cornerIndex] = triangleSharpVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y double deltaX_A = triangleSharpVerticesX[cornerIndex] - triangleSharpVerticesX[previousIndex], deltaY_A = triangleSharpVerticesY[cornerIndex] - triangleSharpVerticesY[previousIndex]; //--- Compute delta A double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A double interpolationFactor_A = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[previousIndex])*deltaX_A + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A triangleTangentPointsX[cornerIndex][1] = triangleSharpVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving triangleTangentPointsY[cornerIndex][1] = triangleSharpVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving double deltaX_B = triangleSharpVerticesX[nextIndex] - triangleSharpVerticesX[cornerIndex], deltaY_B = triangleSharpVerticesY[nextIndex] - triangleSharpVerticesY[cornerIndex]; //--- Compute delta B double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B double interpolationFactor_B = ((triangleArcCentersX[cornerIndex] - triangleSharpVerticesX[cornerIndex])*deltaX_B + (triangleArcCentersY[cornerIndex] - triangleSharpVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B triangleTangentPointsX[cornerIndex][0] = triangleSharpVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving triangleTangentPointsY[cornerIndex][0] = triangleSharpVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving triangleArcStartAngles[cornerIndex] = MathArctan2(triangleTangentPointsY[cornerIndex][1] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][1] - triangleArcCentersX[cornerIndex]); //--- Set start angle triangleArcEndAngles[cornerIndex] = MathArctan2(triangleTangentPointsY[cornerIndex][0] - triangleArcCentersY[cornerIndex], triangleTangentPointsX[cornerIndex][0] - triangleArcCentersX[cornerIndex]); //--- Set end angle } } bool AngleInArcSweep(int cornerIndex, double angle) { double twoPi = 2.0 * M_PI; //--- Define two pi constant double startAngleMod = MathMod(triangleArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle double endAngleMod = MathMod(triangleArcEndAngles[cornerIndex] + twoPi, twoPi); //--- Modulo end angle angle = MathMod(angle + twoPi, twoPi); //--- Modulo angle double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi); //--- Compute CCW span if(ccwSpan <= M_PI) { //--- Check if short way is CCW double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi); //--- Compute relative angle return(relativeAngle <= ccwSpan + 1e-6); //--- Return if within CCW span } else { //--- Else short way is CW double cwSpan = twoPi - ccwSpan; //--- Compute CW span double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi); //--- Compute relative angle return(relativeAngle <= cwSpan + 1e-6); //--- Return if within CW span } }
Мы начинаем с функции "PrecomputeTriangleGeometry", чтобы подготовить геометрические данные для рендеринга закругленного треугольника на объекте canvas с высоким разрешением, присвоив коэффициент суперсэмплирования локальной переменной, масштабировав базовые положения, ширину и высоту на основе входных данных, чтобы сохранить пропорции в высоком разрешении, и определив три острые вершины: верхнюю в центре по оси X с основанием по оси Y, нижнюю левую в основании по оси X с добавленной высотой и нижнюю правую в основании по оси X плюс ширина с той же высотой. Затем мы масштабируем радиус угла и проходим циклом по каждому из трех углов, используя "cornerIndex", вычисляя предыдущий и следующий индексы по модулю 3 для циклической обработки, вычисляя и нормализуя векторы ребер A (от предыдущего к текущему) и B (от текущего к следующему), выводя внешние нормали путем поворота на 90 градусов, и формируя биссектрису угла путем суммирования и нормализации нормалей с возвратом к одной нормали, если длина близка к нулю, чтобы избежать ошибок деления.
Для позиционирования центра дуги мы вычисляем косинус внутреннего угла из скалярного произведения отрицательного ребра, ограничиваем его, находим половинный угол и его синус (с минимумом, чтобы предотвратить равенство нулю), вычисляем расстояние вдоль биссектрисы как радиус, деленный на синус, и устанавливаем центры дуг, смещаясь относительно вершины. Затем мы проецируем центр дуги на каждое смежное ребро, чтобы найти точки касания: для входящего ребра (A) используя векторную проекцию для хранения в индексе 1 массивов касательных, и для выходящего ребра (B) — в индексе 0, обеспечивая плавные переходы между прямыми сторонами и дугами. Наконец, мы задаем начальный и конечный углы для каждого пролета дуги, используя MathArctan2 на смещениях касательных от центра, что определяет точный угловой диапазон для последующих проверок пикселей во время заливки и обводки контуров. Это делает предварительные вычисления необходимыми для точного, векторно-управляемого скругления без искажений.
В функции "AngleInArcSweep" мы нормализуем начальный, конечный и входной углы до значений от 0 до 2 pi с помощью MathMod и сложения, вычисляем хорду против часовой стрелки, и если она равна pi или меньше (короткая дуга), проверяем относительный угол от начала. В противном случае используем хорду по часовой стрелке и проверяем от конца, добавляя небольшой эпсилон для допуска по числам с плавающей запятой, что позволяет надежно определить, попадает ли угол точки в пределы дуги независимо от направления. Далее создадим параметрические вычислительные функции.
void FillRoundedTriangleHiRes(uint fillColor) { double minY = triangleSharpVerticesY[0], maxY = triangleSharpVerticesY[0]; //--- Initialize min and max Y for(int i = 1; i < 3; i++) { //--- Loop over vertices if(triangleSharpVerticesY[i] < minY) minY = triangleSharpVerticesY[i]; //--- Update min Y if(triangleSharpVerticesY[i] > maxY) maxY = triangleSharpVerticesY[i]; //--- Update max Y } int yStart = (int)MathCeil(minY); //--- Compute start Y int yEnd = (int)MathFloor(maxY); //--- Compute end Y for(int y = yStart; y <= yEnd; y++) { //--- Loop over scanlines double scanlineY = (double)y + 0.5; //--- Set scanline Y position double xIntersections[12]; //--- Declare intersections array int intersectionCount = 0; //--- Initialize intersection count for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { //--- Loop over straight edges int nextIndex = (edgeIndex + 1) % 3; //--- Get next index double startX = triangleTangentPointsX[edgeIndex][0], startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end tangent double edgeMinY = (startY < endY) ? startY : endY; //--- Compute edge min Y double edgeMaxY = (startY > endY) ? startY : endY; //--- Compute edge max Y if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue; //--- Skip if outside edge Y if(MathAbs(endY - startY) < 1e-12) continue; //--- Skip if horizontal double interpolationFactor = (scanlineY - startY) / (endY - startY); //--- Compute factor if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX); //--- Add intersection X } for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) { //--- Loop over corner arcs double centerX = triangleArcCentersX[cornerIndex], centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center double radius = (double)triangleCornerRadiusPixels * supersamplingFactor; //--- Get scaled radius double deltaY = scanlineY - centerY; //--- Compute delta Y if(MathAbs(deltaY) > radius) continue; //--- Skip if outside radius double deltaX = MathSqrt(radius*radius - deltaY*deltaY); //--- Compute delta X double candidates[2]; //--- Declare candidates array candidates[0] = centerX - deltaX; //--- Set left candidate candidates[1] = centerX + deltaX; //--- Set right candidate for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX); //--- Compute angle if(AngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep xIntersections[intersectionCount++] = candidates[candidateIndex]; //--- Add intersection } } for(int a = 0; a < intersectionCount - 1; a++) //--- Sort intersections (bubble sort) for(int b = a + 1; b < intersectionCount; b++) //--- Inner loop for sorting if(xIntersections[a] > xIntersections[b]) { //--- Check if swap needed double temp = xIntersections[a]; //--- Temporary store xIntersections[a] = xIntersections[b]; //--- Swap values xIntersections[b] = temp; //--- Complete swap } for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs int xLeft = (int)MathCeil(xIntersections[pairIndex]); //--- Compute left X int xRight = (int)MathFloor(xIntersections[pairIndex + 1]); //--- Compute right X for(int x = xLeft; x <= xRight; x++) //--- Loop over horizontal span triangleHighResCanvas.PixelSet(x, y, fillColor); //--- Set pixel with fill color } } } void DrawRoundedTriangleBorderHiRes(uint borderColor) { int scaledThickness = triangleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) { //--- Loop over edges int nextIndex = (edgeIndex + 1) % 3; //--- Get next index double startX = triangleTangentPointsX[edgeIndex][0], startY = triangleTangentPointsY[edgeIndex][0]; //--- Get start tangent double endX = triangleTangentPointsX[nextIndex][1], endY = triangleTangentPointsY[nextIndex][1]; //--- Get end tangent DrawTriStraightEdge(startX, startY, endX, endY, scaledThickness, borderColor); //--- Draw straight edge } for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) //--- Loop over corners DrawTriCornerArcPrecise(cornerIndex, scaledThickness, borderColor); //--- Draw corner arc } void DrawTriStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint borderColor) { double deltaX = endX - startX; //--- Compute delta X double deltaY = endY - startY; //--- Compute delta Y double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY); //--- Compute edge length if(edgeLength < 1e-6) return; //--- Return if length too small double perpendicularX = -deltaY / edgeLength; //--- Compute perpendicular X double perpendicularY = deltaX / edgeLength; //--- Compute perpendicular Y double edgeDirectionX = deltaX / edgeLength; //--- Compute edge direction X double edgeDirectionY = deltaY / edgeLength; //--- Compute edge direction Y double halfThickness = (double)thickness / 2.0; //--- Compute half thickness double extensionLength = 1.5; //--- Set extension length double extendedStartX = startX - edgeDirectionX * extensionLength; //--- Extend start X double extendedStartY = startY - edgeDirectionY * extensionLength; //--- Extend start Y double extendedEndX = endX + edgeDirectionX * extensionLength; //--- Extend end X double extendedEndY = endY + edgeDirectionY * extensionLength; //--- Extend end Y double verticesX[4], verticesY[4]; //--- Declare vertices arrays verticesX[0] = extendedStartX - perpendicularX * halfThickness; verticesY[0] = extendedStartY - perpendicularY * halfThickness; //--- Set vertex 0 verticesX[1] = extendedStartX + perpendicularX * halfThickness; verticesY[1] = extendedStartY + perpendicularY * halfThickness; //--- Set vertex 1 verticesX[2] = extendedEndX + perpendicularX * halfThickness; verticesY[2] = extendedEndY + perpendicularY * halfThickness; //--- Set vertex 2 verticesX[3] = extendedEndX - perpendicularX * halfThickness; verticesY[3] = extendedEndY - perpendicularY * halfThickness; //--- Set vertex 3 FillQuadrilateral(triangleHighResCanvas, verticesX, verticesY, borderColor); //--- Fill quadrilateral for edge } void DrawTriCornerArcPrecise(int cornerIndex, int thickness, uint borderColor) { double centerX = triangleArcCentersX[cornerIndex], centerY = triangleArcCentersY[cornerIndex]; //--- Get arc center double radius = (double)triangleCornerRadiusPixels * supersamplingFactor; //--- Get scaled radius int halfThickness = thickness / 2; //--- Compute half thickness double outerRadius = radius + halfThickness; //--- Compute outer radius double innerRadius = radius - halfThickness; //--- Compute inner radius if(innerRadius < 0) innerRadius = 0; //--- Set inner radius to zero if negative int pixelRange = (int)(outerRadius + 2); //--- Compute pixel range for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) { //--- Loop over delta Y for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) { //--- Loop over delta X double distance = MathSqrt((double)(deltaX*deltaX + deltaY*deltaY)); //--- Compute distance if(distance < innerRadius || distance > outerRadius) continue; //--- Skip if outside radii double angle = MathArctan2((double)deltaY, (double)deltaX); //--- Compute angle if(AngleInArcSweep(cornerIndex, angle)) { //--- Check if in arc sweep int pixelX = (int)MathRound(centerX + deltaX); //--- Round to pixel X int pixelY = (int)MathRound(centerY + deltaY); //--- Round to pixel Y if(pixelX >= 0 && pixelX < triangleHighResWidth && pixelY >= 0 && pixelY < triangleHighResHeight) //--- Check if within bounds triangleHighResCanvas.PixelSet(pixelX, pixelY, borderColor); //--- Set pixel } } } }
Здесь мы реализуем функцию "FillRoundedTriangleHiRes" для отрисовки закрашенной внутренней части скругленного треугольника на объекте canvas высокого разрешения с использованием алгоритма сканирования строк, сначала определяя вертикальные границы от острых вершин с помощью минимального и максимального значений Y, а затем перебирая в цикле каждое целое число y со смещением в полпикселя для повышения точности. Для каждой строки сканирования мы собираем точки пересечения по оси x с тремя касательными ребрами с помощью линейной интерполяции, если они находятся в пределах диапазона, а также с угловыми дугами, решая уравнение окружности для deltaX при заданном deltaY, добавляя кандидатов только в том случае, если их углы проходят параметр "AngleInArcSweep", чтобы обеспечить ограничение дуги. Мы сортируем пересечения с помощью пузырьковой сортировки, затем заполняем промежутки между парами, используя PixelSet с параметром fillColor, обеспечивая точное сглаживание, которое использует предварительно вычисленную геометрию для плавных кривых.
Далее, в функции "DrawRoundedTriangleBorderHiRes" мы масштабируем толщину рамки и выполняем цикл по ребрам для рисования прямых сегментов с помощью "DrawTriStraightEdge", а затем угловых дуг с помощью "DrawTriCornerArcPrecise", создавая полный, утолщенный контур. Для построения каждого прямого ребра в функции "DrawTriStraightEdge" мы вычисляем векторы направления и перпендикулярные векторы от точек касания, слегка удлиняем конечные точки для бесшовных соединений, определяем четырехугольную полосу, смещенную на половину толщины, и заполняем ее с помощью функции "FillQuadrilateral" для получения равномерной ширины рамки.
Наконец, функция "DrawTriCornerArcPrecise" формирует изогнутое кольцо рамки для каждого угла, вычисляя внутренний и внешний радиусы, перебирая расширенную пиксельную сетку и устанавливая пиксели, если расстояния попадают в кольцо, а углы удовлетворяют условию "AngleInArcSweep", с проверкой границ во избежание переполнения текста за границы, что обеспечивает высококачественные, ровные границы без зазубрин при масштабировании изображения. Теперь можно использовать эти функции для вычисления функции, которую будем использовать для создания закругленного треугольника, то есть для соединения всех элементов.
void DrawRoundedTriangle() { uint backgroundColorARGB = ColorToARGBWithOpacity(triangleBackgroundColor, triangleBackgroundOpacityPercent); //--- Get background ARGB uint borderColorARGB = ColorToARGBWithOpacity(triangleBorderColor, triangleBorderOpacityPercent); //--- Get border ARGB FillRoundedTriangleHiRes(backgroundColorARGB); //--- Fill high-res triangle if(triangleShowBorder && triangleBorderThicknessPixels > 0) //--- Check if border should be shown DrawRoundedTriangleBorderHiRes(borderColorARGB); //--- Draw border on high-res BicubicDownsample(triangleCanvas, triangleHighResCanvas); //--- Downsample to display canvas triangleCanvas.FontSet("Arial", 13, FW_NORMAL); //--- Set font for text string displayText = "Rounded Triangle"; //--- Set display text int textWidth, textHeight; //--- Declare text dimensions triangleCanvas.TextSize(displayText, textWidth, textHeight); //--- Get text size int textPositionX = 10 + (triangleBaseWidthPixels - textWidth) / 2; //--- Compute text X position int textPositionY = 10 + (computedTriangleHeightPixels - textHeight) / 2; //--- Compute text Y position triangleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas }
Мы определяем функцию "DrawRoundedTriangle" для управления отрисовкой скругленного треугольника на объекте canvas, начиная с преобразования цветов фона и рамки в ARGB с интеграцией прозрачности с помощью "ColorToARGBWithOpacity", что позволяет настраивать прозрачность и добавлять глубину фигуре. Для создания внутреннего пространства мы вызываем функцию "FillRoundedTriangleHiRes" с фоновым цветом ARGB, чтобы заполнить объект canvas высокого разрешения с помощью предварительно вычисленной геометрией. Если рамки активированы с помощью функции "triangleShowBorder" и толщина задана положительно, мы вызываем "DrawRoundedTriangleBorderHiRes", чтобы добавить контур с цветовой гаммой ARGB для рамки. Затем мы понижаем дискретизацию изображения с высокого до стандартного canvas, используя функцию "BicubicDownsample" для сглаживания.
Наконец, на стандартном объекте canvas мы настраиваем шрифт с помощью параметра "FontSet" на "Arial" размером 13 и FW_NORMAL, измеряем и центрируем метку "Rounded Triangle" с помощью параметра TextSize и рисуем ее с помощью "TextOut" сплошным черным цветом (0xFF000000) с выравниванием по левому краю, что улучшает идентификацию. Однако вы можете использовать любой цветовой формат по своему выбору. Теперь мы будем использовать ту же логику для отображения треугольника на графике, что и для прямоугольника, и теперь весь фрагмент кода инициализации выглядит следующим образом.
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { supersamplingFactor = supersamplingLevel; //--- Assign supersampling factor from input if(supersamplingFactor < 1) { //--- Check if supersampling factor is less than 1 Print("Warning: supersamplingLevel must be at least 1. Setting to 1."); //--- Print warning message supersamplingFactor = 1; //--- Set supersampling factor to minimum value } computedTriangleHeightPixels = (int)MathRound((double)triangleBaseWidthPixels * triangleHeightAsPercentOfWidth / 100.0); //--- Calculate triangle height based on width and percentage if(computedTriangleHeightPixels < 10) { //--- Check if computed height is too small Print("Warning: Computed triangle height too small (" + string(computedTriangleHeightPixels) + "px). Minimum set to 10."); //--- Print warning message computedTriangleHeightPixels = 10; //--- Set minimum height value } int rectangleCanvasWidth = rectangleWidthPixels + 40; //--- Compute rectangle canvas width with padding int rectangleCanvasHeight = rectangleHeightPixels + 40; //--- Compute rectangle canvas height with padding int rectanglePositionY = shapesPositionY; //--- Set rectangle Y position if(!rectangleCanvas.CreateBitmapLabel(0, 0, rectangleCanvasName, shapesPositionX, rectanglePositionY, rectangleCanvasWidth, rectangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create rectangle canvas bitmap label Print("Error creating rectangle canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } if(!rectangleHighResCanvas.Create(rectangleCanvasName + "_hires", rectangleCanvasWidth * supersamplingFactor, rectangleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res rectangle canvas Print("Error creating rectangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } int triangleCanvasWidth = triangleBaseWidthPixels + 40; //--- Compute triangle canvas width with padding int triangleCanvasHeight = computedTriangleHeightPixels + 40; //--- Compute triangle canvas height with padding int trianglePositionY = rectanglePositionY + rectangleCanvasHeight + shapesGapPixels; //--- Set triangle Y position below rectangle triangleHighResWidth = triangleCanvasWidth * supersamplingFactor; //--- Compute high-res triangle width triangleHighResHeight = triangleCanvasHeight * supersamplingFactor; //--- Compute high-res triangle height if(!triangleCanvas.CreateBitmapLabel(0, 0, triangleCanvasName, shapesPositionX, trianglePositionY, triangleCanvasWidth, triangleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create triangle canvas bitmap label Print("Error creating triangle canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } if(!triangleHighResCanvas.Create(triangleCanvasName + "_hires", triangleHighResWidth, triangleHighResHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res triangle canvas Print("Error creating triangle hi-res canvas: ", GetLastError()); //--- Print error message if creation fails return(INIT_FAILED); //--- Return initialization failure } rectangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear rectangle canvas rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res rectangle canvas triangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear triangle canvas triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res triangle canvas PrecomputeTriangleGeometry(); //--- Precompute triangle geometry DrawRoundedRectangle(); //--- Draw rounded rectangle DrawRoundedTriangle(); //--- Draw rounded triangle rectangleCanvas.Update(); //--- Update rectangle canvas display triangleCanvas.Update(); //--- Update triangle canvas display return(INIT_SUCCEEDED); //--- Return initialization success }
Здесь для визуализации треугольника мы просто используем ту же логику, что и с прямоугольником. Далее мы избавимся от объектов при деинициализации и обработаем события изменения графика следующим образом, перерисовав фигуры.
//+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { rectangleHighResCanvas.Destroy(); //--- Destroy high-res rectangle canvas rectangleCanvas.Destroy(); //--- Destroy rectangle canvas ObjectDelete(0, rectangleCanvasName); //--- Delete rectangle canvas object triangleHighResCanvas.Destroy(); //--- Destroy high-res triangle canvas triangleCanvas.Destroy(); //--- Destroy triangle canvas ObjectDelete(0, triangleCanvasName); //--- Delete triangle canvas object ChartRedraw(); //--- Redraw chart } //+------------------------------------------------------------------+ //| Chart event function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_CHART_CHANGE) { //--- Check for chart change event rectangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear rectangle canvas rectangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res rectangle canvas DrawRoundedRectangle(); //--- Redraw rounded rectangle rectangleCanvas.Update(); //--- Update rectangle canvas display triangleCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear triangle canvas triangleHighResCanvas.Erase(ColorToARGB(clrNONE, 0)); //--- Clear high-res triangle canvas DrawRoundedTriangle(); //--- Redraw rounded triangle triangleCanvas.Update(); //--- Update triangle canvas display } }
В обработчике OnDeinit очищаем ресурсы при завершении работы программы, уничтожая объекты canvas высокого разрешения и стандартные для прямоугольников и треугольников с помощью метода Destroy. После этого удаляем их объекты на графике с помощью ObjectDelete для освобождения памяти и удаления визуальных остатков. Затем вызываем ChartRedraw для обновления графика, чтобы убедиться в отсутствии остаточных артефактов.
Далее, в обработчике OnChartEvent мы реагируем на событие CHARTEVENT_CHART_CHANGE. Он срабатывает при изменении размера графика или изменении его свойств, очищая оба прямоугольных объекта canvas с помощью функции "Erase" с использованием прозрачного ARGB из "clrNONE", перерисовывая скругленный прямоугольник с помощью функции "DrawRoundedRectangle" и обновляя отображение с помощью метода Update. Аналогичным образом очищаем объекты canvas треугольников, перерисовываем их с помощью функции "DrawRoundedTriangle" и обновляем, сохраняя интерактивные визуальные элементы при изменениях графика. После компиляции получаем следующий результат.

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

Заключение
В заключение отметим, что мы рассмотрели векторные методы для рисования скругленных прямоугольников и треугольников на MQL5 с использованием canvas, применяя метод суперсэмплирования для рендеринга со сглаживанием изображения. Мы реализовали заливку методом сканирования строк, геометрические предварительные вычисления для дуг и касательных, а также рисование рамок для создания плавных, настраиваемых фигур. Такой подход закладывает основу для современных элементов пользовательского интерфейса в наших будущих торговых инструментах, поддерживающего входные параметры для установки размеров, радиусов, рамок и прозрачности. В следующей части мы рассмотрим, как можно объединить эти две фигуры, чтобы создать современный пузырь с указателем, который можно использовать в различных приложениях. Следите за обновлениями!
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/21264
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.
Разработка инструментария анализа Price Action (Часть 25): Пробой фракталов по двум EMA
Внедрение в MQL5 практических модулей из других языков (Часть 05): Модуль Logging из Python — ведите логи профессионально
Нелинейные признаки OHLC из эллиптических кривых
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования