MQL5-Handelswerkzeuge (Teil 17): Vektorbasierte abgerundete Rechtecke und Dreiecke
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:
- Vektorbasierte abgerundete Rechtecke und Dreiecke verstehen
- Implementierung in MQL5
- Backtests
- 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.

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.

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:

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:

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.

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
Warnung: Alle Rechte sind von MetaQuotes Ltd. vorbehalten. Kopieren oder Vervielfältigen untersagt.
Dieser Artikel wurde von einem Nutzer der Website verfasst und gibt dessen persönliche Meinung wieder. MetaQuotes Ltd übernimmt keine Verantwortung für die Richtigkeit der dargestellten Informationen oder für Folgen, die sich aus der Anwendung der beschriebenen Lösungen, Strategien oder Empfehlungen ergeben.
Behebung von Barrierefreiheitsproblemen bei MQL5-Handelswerkzeugen (Teil I): Hinzufügen kontextbezogener Sprachnachrichten zu MQL5-Indikatoren
Die MQL5-Standardbibliothek im Überblick (Teil 8): Ein hybrides Handelsjournal mit CFileTxt
MQL5 und Datenverarbeitungspakete integrieren (Teil 7): Entwicklung von Multi-Agenten-Umgebungen für die symbolübergreifende Zusammenarbeit
Python-MetaTrader 5-Strategietester (Teil 05): Strategietests mit mehreren Symbolen und Zeitrahmen
- Freie Handelsapplikationen
- Über 8.000 Signale zum Kopieren
- Wirtschaftsnachrichten für die Lage an den Finanzmärkte
Sie stimmen der Website-Richtlinie und den Nutzungsbedingungen zu.
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?
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