English Русский
preview
MQL5-Handelswerkzeuge (Teil 17): Vektorbasierte abgerundete Rechtecke und Dreiecke

MQL5-Handelswerkzeuge (Teil 17): Vektorbasierte abgerundete Rechtecke und Dreiecke

MetaTrader 5Handel |
11 1
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem vorherigen Artikel (Teil 16) haben wir das Canvas-Dashboard in MetaQuotes Language 5 (MQL5) durch die Einbindung von Anti-Aliasing-Techniken und hochauflösender Darstellung mittels Supersampling verbessert, was zu glatteren Grafiken, Rahmen und Elementen führte. In Teil 17 befassen wir uns mit vektorbasierten Methoden zum Zeichnen abgerundeter Rechtecke und Dreiecke mithilfe von Canvas, wobei wir Supersampling für Ergebnisse mit Kantenglättung einsetzen. Dies schafft die Grundlage für die Erstellung moderner Canvas-Objekte in zukünftigen Tools, indem geometrische Vorberechnungen, Scanline-Füllung und präzise Rahmen berücksichtigt werden. Wir werden die folgenden Themen behandeln:

  1. Vektorbasierte abgerundete Rechtecke und Dreiecke verstehen
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende werden Sie über wiederverwendbare Funktionen für glatte, abgerundete Formen verfügen, die sich nahtlos in komplexe Elemente der Benutzeroberfläche integrieren lassen – legen wir los!


Vektorbasierte abgerundete Rechtecke und Dreiecke verstehen

Der vektorbasierte Ansatz zur Darstellung abgerundeter Rechtecke und Dreiecke nutzt mathematische Beschreibungen von Formen – Punkte, Linien und Kurven – anstelle von Pixelrastern und ermöglicht so skalierbare, auflösungsunabhängige Grafiken, die bei jeder Größe scharf bleiben. Im Gegensatz zu Raster-Methoden, die bei der Skalierung gezackte Kanten (Aliasing) erzeugen können, berechnen Vektortechniken präzise Begrenzungen und Füllungen mithilfe von Gleichungen für Bögen und Tangenten. Dadurch eignen sie sich ideal für UI-Elemente in MQL5, wo eine glatte Darstellung die Benutzerfreundlichkeit verbessert, ohne die Leistung zu beeinträchtigen. Abgerundete Ecken werden erzielt, indem scharfe Ecken durch Kreisbögen ersetzt werden, deren Radien die Krümmung bestimmen. Rahmen lassen sich über versetzte Pfade oder verbreiterte Kanten realisieren, und ein Supersampling verfeinert das Ergebnis zusätzlich, indem es zunächst mit höheren Auflösungen gerendert und anschließend heruntergerechnet wird, um Artefakte zu beseitigen.

Wir planen, hochauflösende Zeichenflächen (Canvas) mit Supersampling zu implementieren, Geometrien für Bögen und Tangenten in Dreiecken vorab zu berechnen, für beide Formen Scanline-Füllung zu verwenden, um präzise Innenbereiche zu gewährleisten, und anpassbare Rahmen mit vektorbasierten geraden Kanten und Eckbögen hinzuzufügen. Wir verarbeiten Benutzereingaben für Abmessungen, Radien, Deckkraftwerte und Farben, um flexible, kantenglättete Formen zu erstellen, die sich für moderne Handelsoberflächen eignen. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

ABGERUNDETE DREIECKE UND RECHTECKE


Implementierung in MQL5

Um das Programm in MQL5 zu erstellen, öffnen wir den MetaEditor, gehen zum Navigator, suchen den Ordner Experts, klicken auf die Registerkarte Neu und folgen den Anweisungen, um die Datei zu erstellen. Sobald sie erstellt ist, müssen wir in der Programmierumgebung einige Eingabeparameter und globale Variablen deklarieren, die wir im gesamten Programm verwenden werden.

//+------------------------------------------------------------------+
//|                           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

Wir beginnen die Implementierung mit dem Einbinden der Bibliothek Canvas mit dem Makro #include <Canvas\Canvas.mqh>, das wichtige Klassen und Methoden zum Erstellen und Bearbeiten von grafischen Zeichenflächen in MQL5 bereitstellt und es uns ermöglicht, benutzerdefinierte Formen wie Rechtecke und Dreiecke direkt auf dem Chart zu zeichnen.

Als Nächstes ordnen wir die Benutzereingaben zur besseren Konfiguration in logische Gruppen ein: die Gruppe Position mit Parametern für die X- und Y-Koordinaten der Formen sowie den Abstand zwischen ihnen; die Gruppe Rechteck, in der Breite, Höhe, Eckenradius, Rahmenoptionen einschließlich Sichtbarkeit, Dicke, Farbe und Deckkraft sowie Hintergrundfarbe und -deckkraft festgelegt werden; die Gruppe Dreieck, die in ähnlicher Weise die Basisbreite, die Höhe als Prozentsatz der Breite (standardmäßig 86,6 für gleichseitige Proportionen), den Eckenradius sowie entsprechende Rahmen- und Hintergrundeinstellungen festlegt; und die Gruppe Allgemein mit dem Supersampling-Level zur Steuerung der Anti-Aliasing-Qualität (1 für keine Glättung, höhere Werte wie 4 für verbesserte Glättung).

Um die Darstellung zu unterstützen, deklarieren wir globale Canvas-Objekte sowohl für die Standard- als auch für die hochauflösende Version des Rechtecks und des Dreiecks und weisen ihnen zur Identifizierung Namen wie RoundedRectCanvas und RoundedTriCanvas zu. Abschließend definieren wir Variablen zur Speicherung des Supersampling-Faktors, der berechneten Dreieckshöhe sowie Arrays für die Dreiecksgeometrie, einschließlich scharfer Eckpunkte, Bogenmittelpunkte, Tangentialpunkte (als 3×2-Arrays pro Ecke), Start- und Endwinkel in Radianten sowie hochauflösender Abmessungen für die Dreiecksfläche. Nachdem die globalen Variablen festgelegt sind, müssen wir nun einige Hilfsfunktionen deklarieren, die ebenfalls im gesamten Programm verwendet werden.

//+------------------------------------------------------------------+
//| 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
}

Zunächst erstellen wir die Funktion ColorToARGBWithOpacity, die eine Farbe in das ARGB-Format konvertiert und dabei einen angegebenen Deckkraftwert berücksichtigt. Wir extrahieren die roten, grünen und blauen Komponenten mithilfe von Bitverschiebungen, berechnen den Alphakanal, indem wir die Deckkraft auf einen Bereich von 0 bis 255 skalieren, und kombinieren diese zu einem einzigen uint-Wert, wodurch transparente Füllungen und Rahmen in unseren Formen möglich werden. Als Nächstes implementieren wir die Funktion BicubicDownsample, um beim Downsampling von der hohen Auflösung auf die Ziel-Canvas-Fläche ein Anti-Aliasing durchzuführen. Wir ermitteln die Zielabmessungen, durchlaufen jedes Pixel in einer Schleife, ordnen es dem supersampelten Quellbereich zu, summieren die gewichteten ARGB-Komponenten der Subpixel (führt ein supersampling-basiertes Mittelwert-Downsampling durch) und berechnen, sofern Samples vorhanden sind, die Endwerte, bevor wir das Pixel festlegen. Dadurch werden Kanten geglättet, indem Details aus der höheren Auflösung gemittelt werden.

Um Winkelberechnungen einheitlich zu handhaben, definieren wir die Funktion NormalizeAngle, die den Winkel modulo 2π normiert und negative Werte anpasst, um sicherzustellen, dass alle Winkel im Bereich von 0 bis 2 Pi liegen, was zuverlässige Vergleiche bei der Bogenwiedergabe ermöglicht. Anschließend fügen wir die Funktion IsAngleBetween hinzu, um zu prüfen, ob ein Winkel innerhalb eines Start-End-Bereichs liegt. Dabei werden die Eingaben normalisiert, die normalisierte Spannweite und die relative Position berechnet und true zurückgegeben, wenn der Winkel innerhalb des Bereichs liegt. Dies ist entscheidend für die präzise Einbeziehung von Pixeln in gekrümmte Rahmen ohne Überzeichnung oder Lücken. Zusätzlich zu diesen Winkeloperationen benötigen wir außerdem eine Funktion zum Ausfüllen einer viereckigen Form.

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
      }
   }
}

Wir implementieren die Funktion FillQuadrilateral, um gefüllte Vierecke auf der Zeichenfläche mithilfe eines Scanline-Algorithmus darzustellen. Dieser gewährleistet eine präzise, vektorbasierte Füllung für Formen wie Rahmen oder Körper, ohne auf integrierte Methoden zurückzugreifen, bei denen die Kontrolle möglicherweise nicht ausreichend ist. Um dies zu erreichen, ermitteln wir zunächst die vertikalen Grenzen, indem wir aus den Eingabeknoten verticesY die minimalen und maximalen Y-Koordinaten ermitteln, und berechnen anschließend die Start- und Endwerte der Scanlines mithilfe der Funktionen ceiling und floor, um eine vollständige Abdeckung zu gewährleisten. Für jede Scanlinie y verschieben wir den Punkt um ein halbes Pixel um scanlineY, um eine Subpixelgenauigkeit zu erreichen und so das Anti-Aliasing zu unterstützen. Anschließend erfassen wir bis zu 8 x-Schnittpunkte, indem wir entlang jeder der vier Kanten interpolieren (unter Verwendung des Modulo-Operators für zyklische Abgeschlossenheit), sofern die Scanlinie die Kante vertikal schneidet, wobei horizontale Kanten sowie Interpolationsfaktoren außerhalb des gültigen Bereichs übersprungen werden.

Wir sortieren diese Schnittpunkte mit einem Bubble-Sort für kleine Arrays und füllen anschließend die horizontalen Bereiche zwischen den gepaarten x-Werten, indem wir die Pixel zwischen xLeft und xRight auf jeder Scanline mit der Methode PixelSet mit fillColor ausfüllen. Diese Methode ist für die Darstellung nichtkonvexer oder unregelmäßiger Vierecke beim hochauflösenden Rendering von entscheidender Bedeutung, da sie die Füllung Pixel für Pixel exakt berechnet und so glatte Kanten bei abgerundeten Formen ermöglicht, indem verdickte Randstreifen ohne Überlappungen oder Lücken ausgefüllt werden.

Falls Sie sich fragen, was es mit diesem Scanline-Algorithmus auf sich hat, lassen Sie uns kurz erklären, worum es dabei geht, damit Sie einen Überblick bekommen. Dieser Algorithmus verarbeitet das Bild von links nach rechts und scannt dabei jeweils eine horizontale Zeile, anstatt einzelne Pixel zu bearbeiten. Es erfasst alle Schnittpunkte der Kanten entlang jeder Scanlinie und füllt das Polygon aus, indem es die Bereiche zwischen den Schnittpunktpaaren einfärbt.

Man kann sich das so vorstellen, als würde man mit einem einzigen Stift eine gerade Linie über eine Form auf Papier ziehen: Man beginnt an der linken Begrenzung und bewegt sich nach rechts, wobei man kontinuierlich zeichnet; sobald man jedoch auf einen Schnittpunkt mit der Polygonbegrenzung stößt, hält man entsprechend an oder setzt das Zeichnen fort. Der Algorithmus folgt demselben Prinzip. In der folgenden Abbildung wird dieses Verhalten veranschaulicht: Die roten Punkte stellen die Eckpunkte des Polygons dar, während die blauen Punkte die Schnittpunkte entlang der Scanlinie kennzeichnen.

SCANLINE-ALGORITHMUS

Nachdem das erledigt ist, können wir diese Funktionen nutzen, um die abgerundeten Formen zu erstellen. Wir beginnen mit einem Rechteck. Auch hierfür werden wir einige Hilfsfunktionen benötigen, um unseren Code modular zu gestalten.

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
      }
   }
}

Zunächst implementieren wir die Funktion FillRoundedRectangleHiRes, um den gefüllten Bereich eines abgerundeten Rechtecks auf der hochauflösenden Zeichenfläche darzustellen. Zunächst füllen wir den zentralen rechteckigen Bereich aus, wobei wir die Ecken auslassen. Als Nächstes fügen wir links und rechts vertikale Streifen hinzu, um die geraden Seiten miteinander zu verbinden. Dieser Ansatz gewährleistet eine lückenlose Abdeckung ohne Überschneidungen. Um die abgerundeten Ecken fertigzustellen, rufen wir für jeden Quadranten die Funktion FillCircleQuadrant auf. Wir übergeben den entsprechenden Mittelpunkt, den Radius, die Füllfarbe und die Quadrantennummer (1 für oben rechts, 2 für oben links usw.). Diese Funktion durchläuft einen leicht überdimensionierten Pixelbereich, prüft anhand einer Abstandsberechnung, ob sich Punkte innerhalb des Quadranten und des Radius befinden, und markiert die entsprechenden Pixel. Dadurch entstehen präzise Viertelkreis-Füllungen, die sich nahtlos in die Streifen einfügen.

Als Nächstes erstellen wir die Funktion DrawRoundedRectangleBorderHiRes zur Darstellung von Rahmen, die Dicke mittels Supersampling skalieren, die vier geraden Kanten oben, unten, links und rechts mit DrawRectStraightEdge zeichnen und die Eckbögen mit DrawRectCornerArcPrecise unter Verwendung vordefinierter Winkel in Radianten rendern (z. B. pi bis 1,5 pi für die obere linke Ecke), was eine gleichmäßige Krümmung und kantenglättete Kanten gewährleistet. In DrawRectStraightEdge berechnen wir Vektorrichtungen und Senkrechten vom Start- zum Endpunkt, verlängern die Linie geringfügig, um bessere Eckverbindungen zu erzielen, definieren einen viereckigen Streifen mit Versätzen in halber Dicke und füllen diesen mithilfe der zuvor definierten Viereckfunktion aus, wodurch dicke, glatte, gerade Rahmen entstehen, die perfekt mit den Bögen abschließen.

Schließlich bildet DrawRectCornerArcPrecise einen Ring zwischen Innen- und Außenradius, indem es die Pixel nacheinander durchläuft, Abstände und Winkel mit IsAngleBetween überprüft und die Pixel der Rahmenfarbe nur innerhalb des angegebenen Bogenbereichs setzt – was für hochwertige, glatte, geschwungene Rahmen bei skalierten Darstellungen entscheidend ist. Mit diesen Funktionen können wir nun ein abgerundetes Rechteck zeichnen.

//+------------------------------------------------------------------+
//| 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
}

Hier definieren wir die Funktion DrawRoundedRectangle, um die Darstellung eines abgerundeten Rechtecks auf der Zeichenfläche zu steuern. Zunächst skalieren wir die Positionsversätze, die Breite, die Höhe und den Radius mit dem Supersampling-Faktor, um eine hohe Auflösung und die Bereitschaft für Anti-Aliasing sicherzustellen. Als Nächstes wandeln wir die Hintergrund- und Rahmenfarben in das ARGB-Format um und berücksichtigen dabei die Deckkraft mithilfe der Funktion ColorToARGBWithOpacity. Diese ermöglicht halbtransparente Effekte, die die visuelle Tiefe verstärken, ohne vollständig deckend zu wirken. Um die Form zu erstellen, rufen wir FillRoundedRectangleHiRes mit diesen skalierten Parametern und der Hintergrundfarbe auf, um den Innenbereich auf der hochauflösenden Zeichenfläche auszufüllen. Sind Rahmen über rectangleShowBorder aktiviert und ist die Dicke positiv, rufen wir DrawRoundedRectangleBorderHiRes auf, um den Umriss in der Rahmenfarbe hinzuzufügen.

Anschließend reduzieren wir die Auflösung der hochauflösenden Zeichenfläche mithilfe von BicubicDownsample auf die Standardauflösung und glätten dabei die Details, um ein gleichmäßiges Ergebnis zu erzielen. Abschließend legen wir auf der Standard-Zeichenfläche die Schriftart mit FontSet fest – Arial in Größe 13 und der Schriftstärke FW_NORMAL –, berechnen mithilfe von TextSize die zentrierten Positionen für die Beschriftung Rounded Rectangle, um die Abmessungen zu ermitteln, und zeichnen sie mit TextOut in undurchsichtigem Schwarz (0xFF000000) linksbündig, um zur besseren Übersichtlichkeit eine beschreibende Einblendung zu schaffen. Wir können diese Funktion nun im Ereignis-Handler für die Initialisierung aufrufen, um das abgerundete Rechteck darzustellen.

//+------------------------------------------------------------------+
//| 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
}

In der Ereignisbehandlung von OnInit initialisieren wir das Programm, indem wir die vom Benutzer eingegebene Variable supersamplingLevel der globalen Variablen supersamplingFactor zuweisen, prüfen, ob ihr Wert unter 1 liegt, und sie im Falle eines gültigen Anti-Aliasing auf den Mindestwert zurücksetzen, wobei eine Warnmeldung ausgegeben wird. Als Nächstes berechnen wir die Abmessungen der rechteckigen Zeichenfläche, indem wir zur eingegebenen Breite und Höhe einen Abstand hinzufügen, legen ihre Y-Position anhand von shapesPositionY fest und erstellen die Standard-Zeichenfläche mit CreateBitmapLabel, wobei wir die Chart-ID, das Unterfenster, den Namen, die Position, die Größe sowie COLOR_FORMAT_ARGB_NORMALIZE für die Transparenzunterstützung angeben, Fehler über GetLastError protokollieren und bei Misserfolg INIT_FAILED zurückgeben.

Anschließend erstellen wir die hochauflösende Zeichenfläche mit Create, wobei wir einen Namen mit Suffix, skalierte Abmessungen, multipliziert mit supersamplingFactor, und dasselbe Farbformat verwenden und Fehler wiederum auf ähnliche Weise behandeln. Zur Vorbereitung des Zeichnens löschen wir beide Zeichenflächen mit Erase und übergeben dabei eine transparente ARGB-Farbe aus clrNONE. Abschließend rufen wir DrawRoundedRectangle auf, um die eigentliche Darstellung durchzuführen, aktualisieren die Standard-Canvas-Anzeige mit Update und geben INIT_SUCCEEDED zurück, um die erfolgreiche Einrichtung zu bestätigen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

ABGERUNDETES RECHTECK

Aus der Visualisierung geht hervor, dass wir das abgerundete Rechteck gerendert haben. Nun gilt es, mit einem ähnlichen Ansatz ein abgerundetes Dreieck darzustellen. Als Nächstes wenden wir denselben Ansatz auf ein abgerundetes Dreieck an. Zunächst erstellen wir eine Hilfsfunktion, um die Geometrie des Dreiecks vorab zu berechnen.

//+------------------------------------------------------------------+
//| 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
   }
}

Wir beginnen mit der Funktion PrecomputeTriangleGeometry, um die geometrischen Daten für die Darstellung eines abgerundeten Dreiecks auf der hochauflösenden Zeichenfläche vorzubereiten. Dabei weisen wir den Supersampling-Faktor einer lokalen Variablen zu, skalieren die Basispositionen, die Breite und die Höhe anhand der Eingabewerte, um die Proportionen bei hoher Auflösung beizubehalten, und definieren die drei spitzen Eckpunkte: den oberen am mittleren Basis-X-Punkt mit Basis-Y, den unteren linken Eckpunkt bei der Basis-X-Koordinate mit der zusätzlichen Höhe sowie den unteren rechten Eckpunkt bei der Basis-X-Koordinate plus der Breite bei gleicher Höhe. Als Nächstes skalieren wir den Eckenradius und durchlaufen nacheinander alle drei Ecken mithilfe von cornerIndex, wobei wir den vorherigen und den nächsten Index modulo 3 berechnen, um eine zyklische Verarbeitung zu gewährleisten, und berechnen und normalisieren die Kantenvektoren A (vom vorherigen zum aktuellen) und B (vom aktuellen zum nächsten), leiten durch eine 90-Grad-Drehung die nach außen gerichteten Normalen ab und bilden die Winkelhalbierende durch Summierung und Normalisierung der Normalen, wobei auf eine einzige Normale zurückgegriffen wird, wenn die Länge nahe Null liegt, um Divisionsfehler zu vermeiden.

Um den Bogenmittelpunkt zu bestimmen, berechnen wir den Kosinus des Innenwinkels aus dem negativen Skalarprodukt der Kanten, begrenzen diesen Wert, ermitteln den Halbwinkel und dessen Sinus (mit einem Mindestwert, um Nullwerte zu vermeiden), berechnen den Abstand entlang der Winkelhalbierenden als Radius geteilt durch den Sinus und legen die Bogenmittelpunkte durch Versatz vom Scheitelpunkt fest. Anschließend projizieren wir den Bogenmittelpunkt auf jede angrenzende Kante, um Tangentialpunkte zu ermitteln: Für die vorherige Kante (A) verwenden wir die Vektorprojektion, um den Wert unter Index 1 der Tangential-Arrays zu speichern, und für die folgende Kante (B) unter Index 0, wodurch fließende Übergänge zwischen geraden Seiten und Bögen gewährleistet werden. Schließlich legen wir die Start- und Endwinkel für jeden Bogenverlauf mithilfe von MathArctan2 anhand der Tangentenversätze vom Mittelpunkt fest. Dadurch wird der genaue Winkelbereich für spätere Pixelprüfungen beim Ausfüllen und Einrahmen definiert, was diese Vorberechnung für eine genaue, vektorbasierte Abrundung ohne Verzerrungen unerlässlich macht.

In der Funktion AngleInArcSweep normalisieren wir den Start-, End- und Eingabewinkel mithilfe von MathMod und Additionen auf den Bereich von 0 bis 2 pi, berechnen die Spannweite gegen den Uhrzeigersinn und prüfen, ob diese pi oder kleiner ist (kurzer Bogen); in diesem Fall wird der relative Winkel zum Startpunkt ermittelt; ansonsten verwenden wir die Spannweite im Uhrzeigersinn und prüfen vom Endpunkt aus, wobei wir ein kleines Epsilon als Fließkomma-Toleranz hinzufügen, um unabhängig von der Richtung zuverlässig feststellen zu können, ob der Winkel eines Punktes innerhalb des Bogens liegt. Als Nächstes werden wir die parametrischen Berechnungsfunktionen erstellen.

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
         }
      }
   }
}

Hier implementieren wir die Funktion FillRoundedTriangleHiRes, um den gefüllten Innenbereich eines abgerundeten Dreiecks auf der hochauflösenden Zeichenfläche mithilfe eines Scanline-Algorithmus darzustellen. Dabei werden zunächst die vertikalen Begrenzungen anhand der scharfen Eckpunkte mit den Minimal- und Maximalwerten für die Y-Achse ermittelt; anschließend wird für eine höhere Genauigkeit eine Schleife über jeden ganzzahligen y-Wert mit einem Versatz von einem halben Pixel durchlaufen. Für jede Scanlinie erfassen wir x-Schnittpunkte der drei tangentialen Kanten mittels linearer Interpolation, sofern sie innerhalb des Bereichs liegen, und der Eckbögen durch Auflösen der Kreisgleichung für deltaX bei gegebenem deltaY, wobei Kandidaten nur dann hinzugefügt werden, wenn ihre Winkel die Bedingung AngleInArcSweep erfüllen, um die Begrenzung auf den Bogen sicherzustellen. Wir sortieren Schnittpunkte mit dem Bubble-Sort-Verfahren und füllen anschließend die Bereiche zwischen den Paaren mithilfe von PixelSet mit der Füllfarbe aus. So erzielen wir eine präzise, kantenglättete Abdeckung, die vorberechnete Geometriedaten für glatte Kurven nutzt.

Anschließend skalieren wir in der Funktion DrawRoundedTriangleBorderHiRes die Rahmenstärke und durchlaufen in einer Schleife alle Kanten, um mit DrawTriStraightEdge gerade Segmente zu zeichnen, gefolgt von Eckbögen mit DrawTriCornerArcPrecise, wodurch ein vollständiger, verdickter Umriss entsteht. Um jede gerade Kante in DrawTriStraightEdge zu zeichnen, berechnen wir Richtungs- und Senkrechtvektoren anhand der Tangentialpunkte, verlängern die Endpunkte geringfügig, um nahtlose Übergänge zu gewährleisten, definieren einen um die halbe Dicke versetzten viereckigen Streifen und füllen diesen mit FillQuadrilateral, um eine gleichmäßige Randbreite zu erzielen.

Schließlich erzeugt DrawTriCornerArcPrecise den gekrümmten Randring pro Ecke, indem es Innen- und Außenradien berechnet, ein erweitertes Pixelraster durchläuft und Pixel setzt, wenn die Abstände innerhalb des Rings liegen und die Winkel die Bedingung AngleInArcSweep erfüllen – dabei werden Grenzwertprüfungen durchgeführt, um Überläufe zu vermeiden, wodurch hochwertige, glatte Rahmen bei skalierten Darstellungen gewährleistet werden. Mit diesen Funktionen können wir nun die Funktion berechnen, mit der wir das abgerundete Dreieck erstellen werden – sozusagen, um alles zusammenzufügen.

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
}

Wir definieren die Funktion DrawRoundedTriangle, um die Darstellung eines abgerundeten Dreiecks auf der Zeichenfläche zu steuern. Zunächst wandeln wir die Hintergrund- und Rahmenfarben mithilfe von ColorToARGBWithOpacity in ARGB mit integrierter Deckkraft um, wodurch eine anpassbare Transparenz ermöglicht wird, die der Form Tiefe verleiht. Um den Innenraum zu erstellen, rufen wir FillRoundedTriangleHiRes mit dem ARGB-Hintergrund auf, um die hochauflösende Zeichenfläche mithilfe der vorberechneten Geometrie auszufüllen. Wenn Rahmen über triangleShowBorder aktiviert werden und die Dicke positiv ist, rufen wir DrawRoundedTriangleBorderHiRes auf, um den Umriss mit dem Rahmen-ARGB hinzuzufügen. Anschließend skalieren wir die hochauflösende Darstellung per Downsampling auf die Standard-Zeichenfläche herunter und verwenden dabei BicubicDownsample, um eine glatte Darstellung ohne Treppchenbildung zu erzielen.

Abschließend konfigurieren wir auf der Standard-Zeichenfläche die Schriftart mit FontSet auf Arial, Schriftgröße 13 und FW_NORMAL, passen die Größe der Beschriftung Rounded Triangle mithilfe von TextSize an und zentrieren sie. Anschließend zeichnen wir sie mit TextOut in sattem Schwarz (0xFF000000) linksbündig, um die Erkennbarkeit zu verbessern. Sie können jedoch jedes beliebige Farbformat verwenden. Wir werden nun dieselbe Logik anwenden, um das Dreieck im Chart darzustellen, wie wir es bereits beim Rechteck getan haben, und nun sieht der gesamte Codeausschnitt zur Initialisierung wie folgt aus.

//+------------------------------------------------------------------+
//| 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
}

Hier wenden wir einfach dieselbe Logik an wie beim Rechteck, um das Dreieck darzustellen. Als Nächstes müssen wir die Objekte bei der Deinitialisierung entfernen und Ereignisse bei Änderungen am Chart wie folgt verarbeiten, indem wir die Formen neu zeichnen.

//+------------------------------------------------------------------+
//| 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
   }
}

In der Ereignisbehandlung von OnDeinit bereinigen wir die Ressourcen beim Beenden des Programms, indem wir die hochauflösenden und Standard-Canvases sowohl für Rechtecke als auch für Dreiecke mit der Methode Destroy auflösen und anschließend deren Diagrammobjekte mit ObjectDelete löschen, um Speicher freizugeben und visuelle Reste zu entfernen. Anschließend rufen wir ChartRedraw auf, um das Diagramm zu aktualisieren und sicherzustellen, dass keine Artefakte mehr sichtbar sind.

Anschließend reagieren wir in OnChartEvent auf das Ereignis CHARTEVENT_CHART_CHANGE – das durch eine Größenänderung des Diagramms oder durch Änderungen an den Eigenschaften ausgelöst wird –, indem wir beide Rechteck-Canvases mit Erase unter Verwendung von transparentem ARGB aus clrNONE löschen, das abgerundete Rechteck über DrawRoundedRectangle neu zeichnen und die Anzeige mit der Methode Update aktualisieren. Ebenso löschen wir die dreieckigen Zeichenflächen, zeichnen sie mit DrawRoundedTriangle neu und aktualisieren sie, sodass die Darstellung bei Diagrammänderungen korrekt aktualisiert wird. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

RENDER VON ABGERUNDETEN DREIECKEN UND RECHTECKEN

Aus der Visualisierung geht hervor, dass wir ein abgerundetes Dreieck und ein Rechteck erstellt und damit unsere Ziele erreicht haben. Nun bleibt nur noch, die Funktionsfähigkeit des Systems zu testen, was im vorangegangenen Abschnitt behandelt wurde.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die resultierende Visualisierung als einzelne GIF-Datei.

BACKTEST-GIF


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir vektorbasierte Methoden zum Zeichnen abgerundeter Rechtecke und Dreiecke in MQL5 unter Verwendung von Canvas untersucht haben, wobei wir Supersampling für die Anti-Aliasing-Darstellung integriert haben. Wir haben die Scanline-Füllung, geometrische Vorberechnungen für Bögen und Tangenten sowie das Zeichnen von Rahmen implementiert, um glatte, anpassbare Formen zu erstellen. Dieser Ansatz schafft die Grundlage für moderne Elemente der Benutzeroberfläche in unseren zukünftigen Trading-Tools und ermöglicht die Eingabe von Größen, Radien, Rahmen und Deckkraftwerten. Im nächsten Teil werden wir untersuchen, wie wir die beiden Formen zu einer modernen Sprechblase mit einem Zeiger kombinieren können, die in verschiedenen Anwendungen eingesetzt werden kann. Bleiben Sie dran!

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21264

Letzte Kommentare | Zur Diskussion im Händlerforum (1)
Nauris Zukas
Nauris Zukas | 13 Mai 2026 in 07:52
Vielen Dank für diesen Artikel! Er ist sehr hilfreich und ausführlich.
Ich versuche, meinem abgerundeten Rechteck einen Abschrägungseffekt (3D-Beleuchtung – weiß oben/links, schwarz unten/rechts) hinzuzufügen.
Ich habe den Abschrägungseffekt an geraden Kanten erfolgreich umgesetzt, habe aber Probleme, sie an den Ecken miteinander zu verbinden.
Siehe die roten Kreise im Bild unten, wo sich die Linien treffen.
Irgendwelche Vorschläge?


//+------------------------------------------------------------------+
//| EFFEKTE MIT ABGESCHRÄGTEN KANTEN
//+------------------------------------------------------------------+
void DrawBeveledEdges(int posX, int posY, int width, int height, int radius, int scaleFactor)
  {
   color lightColor = clrWhite;
   uint lightARGB = ColorToARGBWithOpacity(lightColor, 20);

   color darkColor = clrBlack;
   uint darkARGB = ColorToARGBWithOpacity(darkColor, 35);

   int bevelWidth = 3 * scaleFactor;

// ===== HELLER BEREICH (OBEN LINKS) =====

// Oberer horizontaler Teil
   for(int y = 0; y < bevelWidth; y++)
     {
      for(int x = radius; x < width - radius; x++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, lightARGB);
        }
     }

// Linker vertikaler Teil
   for(int x = 0; x < bevelWidth; x++)
     {
      for(int y = radius; y < height - radius; y++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, lightARGB);
        }
     }

// ARC oben links – NUR helle Farbe
   for(int dy = -radius; dy <= 0; dy++)
     {
      for(int dx = -radius; dx <= 0; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + radius + dx;
            int pixY = posY + radius + dy;

            if(pixX >= posX && pixY >= posY)
               rectangleHighResCanvas.PixelSet(pixX, pixY, lightARGB);
           }
        }
     }

// ARC oben rechts – NUR helle Farbe (obere rechte Ecke)
   for(int dy = -radius; dy <= 0; dy++)
     {
      for(int dx = 0; dx <= radius; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + width - radius + dx;
            int pixY = posY + radius + dy;

            if(pixX < posX + width && pixY >= posY)
               rectangleHighResCanvas.PixelSet(pixX, pixY, lightARGB);
           }
        }
     }

// ===== DUNKLE KANTE (UNTEN RECHTS) =====

// Unterer horizontaler Teil
   for(int y = height - bevelWidth; y < height; y++)
     {
      for(int x = radius; x < width - radius; x++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, darkARGB);
        }
     }

// Rechter vertikaler Teil
   for(int x = width - bevelWidth; x < width; x++)
     {
      for(int y = radius; y < height - radius; y++)
        {
         rectangleHighResCanvas.PixelSet(posX + x, posY + y, darkARGB);
        }
     }

// ARC unten rechts – NUR dunkle Farbe
   for(int dy = 0; dy <= radius; dy++)
     {
      for(int dx = 0; dx <= radius; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + width - radius + dx;
            int pixY = posY + height - radius + dy;

            if(pixX < posX + width && pixY < posY + height)
               rectangleHighResCanvas.PixelSet(pixX, pixY, darkARGB);
           }
        }
     }

// ARC unten links – NUR dunkle Farbe (untere linke Ecke)
   for(int dy = 0; dy <= radius; dy++)
     {
      for(int dx = -radius; dx <= 0; dx++)
        {
         double dist = MathSqrt((double)(dx*dx + dy*dy));

         if(dist >= radius - bevelWidth && dist <= radius + 1)
           {
            int pixX = posX + radius + dx;
            int pixY = posY + height - radius + dy;

            if(pixX >= posX && pixY < posY + height)
               rectangleHighResCanvas.PixelSet(pixX, pixY, darkARGB);
           }
        }
     }
  }
//+------------------------------------------------------------------+


P.S. - Themenvorschläge für zukünftige Artikel:
Schlagschatten- und Glüheffekte – wie man UI-Elementen Tiefe verleiht
Farbverläufe – 3D-Effekte durch Farbübergänge erzeugen
Anti-Aliasing-Techniken – Kanten glätten für ein professionelles Erscheinungsbild
Innenschatten – 3D-Abschrägungseffekte verstärken
Textur- und Rauschmuster – flachen Formen mehr Realismus verleihen
Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil I): Hinzufügen kontextbezogener Sprachnachrichten zu MQL5-Indikatoren Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil I): Hinzufügen kontextbezogener Sprachnachrichten zu MQL5-Indikatoren
Dieser Artikel befasst sich mit einer auf Barrierefreiheit ausgerichteten Erweiterung, die über die standardmäßigen Terminal-Warnmeldungen hinausgeht und mithilfe der MQL5-Ressourcenverwaltung kontextbezogenes Sprachfeedback bereitstellt. Anstelle von allgemeinen Signaltönen vermittelt der Indikator, was geschehen ist und warum, sodass Trader die Marktgeschehnisse nachvollziehen können, ohne sich ausschließlich auf visuelle Beobachtungen verlassen zu müssen. Dieser Ansatz ist besonders für sehbehinderte Trader von großem Nutzen, kommt aber auch vielbeschäftigten Nutzern oder Nutzern, die mehrere Aufgaben gleichzeitig erledigen und eine freihändige Bedienung bevorzugen.
Die MQL5-Standardbibliothek im Überblick (Teil 8): Ein hybrides Handelsjournal mit CFileTxt Die MQL5-Standardbibliothek im Überblick (Teil 8): Ein hybrides Handelsjournal mit CFileTxt
In diesem Artikel befassen wir uns mit den Dateiverarbeitungsklassen der MQL5-Standardbibliothek, um ein robustes Reporting-Modul zu entwickeln, das automatisch Excel-kompatible CSV-Dateien generiert. Dabei unterscheiden wir klar zwischen manuell ausgeführten Trades und algorithmisch ausgeführten Orders und schaffen damit die Grundlage für ein zuverlässiges, auditierbares Trade-Reporting.
MQL5 und Datenverarbeitungspakete integrieren (Teil 7): Entwicklung von Multi-Agenten-Umgebungen für die symbolübergreifende Zusammenarbeit MQL5 und Datenverarbeitungspakete integrieren (Teil 7): Entwicklung von Multi-Agenten-Umgebungen für die symbolübergreifende Zusammenarbeit
Der Artikel stellt eine vollständige Python-MQL5-Integration für den Multi-Agenten-Handel vor: MT5-Datenerfassung, Berechnung von Indikatoren, Entscheidungen pro Agent und ein gewichteter Konsens, der zu einer einzigen Handelsaktion führt. Die Signale werden im JSON-Format gespeichert, über Flask bereitgestellt und von einem MQL5-Expert Advisor zur Ausführung mit Positionsgrößenbestimmung und aus dem ATR abgeleiteten SL/TP verarbeitet. Flask-Routen ermöglichen eine sichere Steuerung des Lebenszyklus und eine Statusüberwachung.
Python-MetaTrader 5-Strategietester (Teil 05): Strategietests mit mehreren Symbolen und Zeitrahmen Python-MetaTrader 5-Strategietester (Teil 05): Strategietests mit mehreren Symbolen und Zeitrahmen
Dieser Artikel stellt einen MetaTrader 5-kompatiblen Backtesting-Workflow vor, der sich auf verschiedene Symbole und Zeitrahmen hinweg skaliert. Wir nutzen den HistoryManager, um die Datenerfassung zu parallelisieren, Kursbalken und Ticks aus allen Zeitrahmen zu synchronisieren und symbolisolierte OnTick-Handler in Threads auszuführen. Sie erfahren, wie sich Modellierungsmodi auf Geschwindigkeit und Genauigkeit auswirken, wann Sie sich auf Terminaldaten verlassen können, wie Sie den I/O-Aufwand durch ereignisgesteuerte Aktualisierungen reduzieren und wie Sie einen vollständigen Multiwährungs-Trading-Roboter erstellen.