English Русский
preview
MQL5-Handelswerkzeuge (Teil 18): Abgerundete Sprechblasen mit Orientierungssteuerung

MQL5-Handelswerkzeuge (Teil 18): Abgerundete Sprechblasen mit Orientierungssteuerung

MetaTrader 5Beispiele |
13 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem vorherigen Artikel (Teil 17) haben wir vektorbasierte Methoden zum Zeichnen von abgerundeten Rechtecken und Dreiecken in MetaQuotes Language 5 (MQL5) unter Verwendung von Canvas untersucht, wobei Supersampling zur Kantenglättung zum Einsatz kam. In Teil 18 kombinieren wir diese Formen, um abgerundete Sprechblasen mit Ausrichtungssteuerung zu erstellen, sodass die Zeiger nach oben, unten, links oder rechts zeigen können. Diese Integration umfasst anpassbare Positionen, Rahmen und Deckkraftwerte und bietet damit vielseitige UI-Elemente für Handelsoberflächen. Wir werden die folgenden Themen behandeln:

  1. Grundlagen abgerundeter Sprechblasen mit Ausrichtung
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende werden Sie über ein funktionsfähiges Sprechblasensystem verfügen, das sich für die Anpassung in fortgeschrittenen Anwendungen eignet – legen wir los!


Grundlagen abgerundeter Sprechblasen mit Ausrichtung

Die abgerundete Sprechblase mit Ausrichtungssteuerung kombiniert einen abgerundeten rechteckigen Hauptteil mit einem dreieckigen Zeiger. Dies ermöglicht eine dynamische Positionierung des Zeigers nach oben, unten, links oder rechts über eine Enumeration und schafft so vielseitige UI-Elemente für Warnmeldungen oder Tooltips in Handelsoberflächen. Die Ausrichtung bestimmt das Layout, indem der Hauptteil relativ zum Zeiger verschoben wird, Basisversätze zur Ausrichtung berücksichtigt und nahtlose Übergänge zwischen den Formen gewährleistet werden, um optische Brüche zu vermeiden. Dieser Vektoransatz ermöglicht eine skalierbare, hochwertige Darstellung mit Supersampling für kantenglättete Kanten und verbessert so die Lesbarkeit und Ästhetik in MQL5-Anwendungen. Unser Plan sieht vor, Hauptteil- und Zeigergeometrien auf der Grundlage der Ausrichtung vorab zu berechnen, die kombinierte Form mithilfe angepasster Scanline-Algorithmen für horizontale bzw. vertikale Scanlines auszufüllen und segmentierte Umrandungen mit verlängerten Kanten für nahtlose Übergänge zu zeichnen. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

VERSCHIEDENE ZIELE BEI DER ANORDNUNG VON SPRECHBLASEN


Implementierung in MQL5

Um das Programm in MQL5 zu erweitern, müssen wir neue Definitionen, globale Variablen und Eingabeparameter hinzufügen, um die neue Sprechblase mit Ausrichtungsfunktionen zu steuern.

//+------------------------------------------------------------------+
//|                           Rounded Rectangle & Triangle PART2.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

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum BUBBLE_ORIENTATION {
   ORIENT_UP    = 0, // Pointer faces up
   ORIENT_DOWN  = 1, // Pointer faces down
   ORIENT_LEFT  = 2, // Pointer faces left
   ORIENT_RIGHT = 3  // Pointer faces right
};

input group "Bubble Shape"
input int              bubbleBodyWidthPixels            = 250;          // Bubble body width
input int              bubbleBodyHeightPixels           = 100;          // Bubble body height
input int              bubbleBodyCornerRadiusPixels     = 5;            // Bubble body corner radius
input int              bubblePointerBaseWidthPixels     = 60;           // Bubble pointer base width
input int              bubblePointerHeightPixels        = 40;           // Bubble pointer height
input int              bubblePointerApexRadiusPixels    = 12;           // Bubble pointer apex radius
input int              bubblePointerBaseOffsetPixels    = 0;            // Bubble pointer offset from center (0=centered, +/-=shift)
input BUBBLE_ORIENTATION bubblePointerOrientation       = ORIENT_UP;    // Bubble pointer orientation
input bool             bubbleShowBorder                 = true;         // Show bubble border
input int              bubbleBorderThicknessPixels      = 2;            // Bubble border thickness
input color            bubbleBorderColor                = clrGreen;     // Bubble border color
input int              bubbleBorderOpacityPercent       = 80;           // Bubble border opacity (0-100%)
input color            bubbleBackgroundColor            = clrGreen;     // Bubble background color
input int              bubbleBackgroundOpacityPercent   = 30;           // Bubble background opacity (0-100%)

input group "General"
input double borderExtensionMultiplier        = 0.23;    // Border extension multiplier (fraction of thickness)

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CCanvas bubbleCanvas,    bubbleHighResCanvas;                   //--- Declare bubble canvas objects

string  bubbleCanvasName    = "BubbleCanvas";                   //--- Set bubble canvas name

double bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight, bubbleBodyBottom; //--- Store bubble body coordinates
double bubblePointerVerticesX[3], bubblePointerVerticesY[3];    //--- Store bubble pointer vertices X and Y
double bubblePointerArcCentersX[3], bubblePointerArcCentersY[3]; //--- Store bubble pointer arc centers X and Y
double bubblePointerTangentPointsX[3][2], bubblePointerTangentPointsY[3][2]; //--- Store bubble pointer tangent points X and Y
double bubblePointerArcStartAngles[3], bubblePointerArcEndAngles[3]; //--- Store bubble pointer arc start and end angles
int    bubblePointerApexIndex;                                  //--- Store bubble pointer apex index
double bubblePointerBaseStart, bubblePointerBaseEnd;            //--- Store bubble pointer base start and end
bool   bubbleIsHorizontalOrientation;                           //--- Store if bubble orientation is horizontal

Anschließend definieren wir die Enumeration BUBBLE_ORIENTATION mit Optionen für die Zeigerrichtungen: ORIENT_UP (0), ORIENT_DOWN (1), ORIENT_LEFT (2) und ORIENT_RIGHT (3). Dies ermöglicht eine flexible Steuerung der Ausrichtung der Sprechblase bzw. ihres Zeigers. In der Eingabegruppe Bubble Shape fügen wir Parameter für die Abmessungen des Hauptteils hinzu, wie beispielsweise bubbleBodyWidthPixels und bubbleBodyHeightPixels. Zu den weiteren Parametern gehören der Eckenradius bubbleBodyCornerRadiusPixels, Zeigerangaben wie bubblePointerBaseWidthPixels, bubblePointerHeightPixels und bubblePointerApexRadiusPixels sowie der Versatz bubblePointerBaseOffsetPixels zur Zentrierung. Außerdem berücksichtigen wir die Ausrichtung aus der Enumeration, die Umschaltoptionen für Rahmen, die Dicke, die Farben und die Deckkraft – ähnlich wie bei den vorherigen Formen.

Um den Rahmen präzise anzupassen, haben wir einen allgemeinen Eingabewert borderExtensionMultiplier als Bruchteil der Dicke für die Kantenverlängerungen vorgesehen, um nahtlose Übergänge zu gewährleisten. Globale Variablen werden auf die Sprechblasen-spezifischen Zeichenflächen bubbleCanvas und bubbleHighResCanvas mit dem Namen BubbleCanvas erweitert; wir speichern die Grenzen des Hauptteils als Double-Werte wie bubbleBodyLeft und bubbleBodyTop, die Eckpunkte des Zeigers, Bogenmittelpunkte, Tangenten (3x2-Arrays), Winkel, den Scheitelpunktindex bubblePointerApexIndex, den Start- und Endpunkt der Basis sowie einen booleschen Wert bubbleIsHorizontalOrientation, um die Ausrichtung der Scanlinien dynamisch zu handhaben. Nun müssen wir Funktionen definieren, um die Erstellung der Sprechblase zu steuern. Dabei wenden wir eine ähnliche Logik an wie bei den anderen Formen in der vorherigen Version, und zwar wie folgt. Zunächst werden wir die Geometrie der Blase vorab berechnen.

//+------------------------------------------------------------------+
//| Bubble Shape                                                     |
//+------------------------------------------------------------------+
void PrecomputeBubbleGeometry() {
   int scalingFactor = supersamplingFactor;                     //--- Set scaling factor
   double baseOffset = 10.0 * scalingFactor;                    //--- Set base offset scaled
   
   double centeringAdjustment;                                  //--- Declare centering adjustment

   if(bubblePointerOrientation == ORIENT_UP) {                  //--- Check for up orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = baseOffset;                   //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[1] = bubbleBodyTop;                //--- Set left Y
      
      bubblePointerVerticesX[2] = bubblePointerVerticesX[1] + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[2] = bubbleBodyTop;                //--- Set right Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[2];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_DOWN) {         //--- Check for down orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyWidthPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterX = bubbleBodyLeft + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center X
      
      bubblePointerVerticesX[0] = pointerCenterX;               //--- Set apex X
      bubblePointerVerticesY[0] = bubbleBodyBottom + bubblePointerHeightPixels * scalingFactor; //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set right X
      bubblePointerVerticesY[1] = bubbleBodyBottom;             //--- Set right Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft + actualOffset; //--- Set left X
      bubblePointerVerticesY[2] = bubbleBodyBottom;             //--- Set left Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesX[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesX[1];         //--- Set base end
      
   } else if(bubblePointerOrientation == ORIENT_LEFT) {         //--- Check for left orientation
      bubbleBodyLeft = baseOffset + bubblePointerHeightPixels * scalingFactor; //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = baseOffset;                   //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyLeft;               //--- Set bottom X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerVerticesX[2] = bubbleBodyLeft;               //--- Set top X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[2];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[1];         //--- Set base end
      
   } else {                                                     //--- Handle right orientation
      bubbleBodyLeft = baseOffset;                              //--- Set body left
      bubbleBodyTop = baseOffset;                               //--- Set body top
      bubbleBodyRight = bubbleBodyLeft + bubbleBodyWidthPixels * scalingFactor; //--- Set body right
      bubbleBodyBottom = bubbleBodyTop + bubbleBodyHeightPixels * scalingFactor; //--- Set body bottom

      centeringAdjustment = (bubbleBodyHeightPixels * scalingFactor - bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute centering
      double actualOffset = centeringAdjustment + (bubblePointerBaseOffsetPixels * scalingFactor); //--- Apply offset
      
      double pointerCenterY = bubbleBodyTop + actualOffset + (bubblePointerBaseWidthPixels * scalingFactor) / 2.0; //--- Compute pointer center Y
      
      bubblePointerVerticesX[0] = bubbleBodyRight + bubblePointerHeightPixels * scalingFactor; //--- Set apex X
      bubblePointerVerticesY[0] = pointerCenterY;               //--- Set apex Y
      
      bubblePointerVerticesX[1] = bubbleBodyRight;              //--- Set top X
      bubblePointerVerticesY[1] = bubbleBodyTop + actualOffset; //--- Set top Y
      
      bubblePointerVerticesX[2] = bubbleBodyRight;              //--- Set bottom X
      bubblePointerVerticesY[2] = bubbleBodyTop + actualOffset + bubblePointerBaseWidthPixels * scalingFactor; //--- Set bottom Y
      
      bubblePointerApexIndex = 0;                               //--- Set apex index
      bubblePointerBaseStart = bubblePointerVerticesY[1];       //--- Set base start
      bubblePointerBaseEnd = bubblePointerVerticesY[2];         //--- Set base end
   }

   ComputeBubbleTriangleRoundedCorners();                       //--- Compute rounded corners for bubble pointer
}

void ComputeBubbleTriangleRoundedCorners() {
   double scaledRadius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Scale apex radius

   int cornerIndex = bubblePointerApexIndex;                    //--- Set corner index to apex
   
   int previousIndex = (cornerIndex + 2) % 3;                   //--- Get previous index
   int nextIndex = (cornerIndex + 1) % 3;                       //--- Get next index

   double edgeA_X = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute edge A X
   double edgeA_Y = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute edge A Y
   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 = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute edge B X
   double edgeB_Y = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute edge B Y
   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
   bubblePointerArcCentersX[cornerIndex] = bubblePointerVerticesX[cornerIndex] + bisectorX * distanceToCenter; //--- Set arc center X
   bubblePointerArcCentersY[cornerIndex] = bubblePointerVerticesY[cornerIndex] + bisectorY * distanceToCenter; //--- Set arc center Y

   double deltaX_A = bubblePointerVerticesX[cornerIndex] - bubblePointerVerticesX[previousIndex]; //--- Compute delta A X
   double deltaY_A = bubblePointerVerticesY[cornerIndex] - bubblePointerVerticesY[previousIndex]; //--- Compute delta A Y
   double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A; //--- Compute length squared A
   double interpolationFactor_A = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[previousIndex])*deltaX_A + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[previousIndex])*deltaY_A) / lengthSquared_A; //--- Compute factor A
   bubblePointerTangentPointsX[cornerIndex][1] = bubblePointerVerticesX[previousIndex] + interpolationFactor_A * deltaX_A; //--- Set tangent point X arriving
   bubblePointerTangentPointsY[cornerIndex][1] = bubblePointerVerticesY[previousIndex] + interpolationFactor_A * deltaY_A; //--- Set tangent point Y arriving

   double deltaX_B = bubblePointerVerticesX[nextIndex] - bubblePointerVerticesX[cornerIndex]; //--- Compute delta B X
   double deltaY_B = bubblePointerVerticesY[nextIndex] - bubblePointerVerticesY[cornerIndex]; //--- Compute delta B Y
   double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B; //--- Compute length squared B
   double interpolationFactor_B = ((bubblePointerArcCentersX[cornerIndex] - bubblePointerVerticesX[cornerIndex])*deltaX_B + 
                                   (bubblePointerArcCentersY[cornerIndex] - bubblePointerVerticesY[cornerIndex])*deltaY_B) / lengthSquared_B; //--- Compute factor B
   bubblePointerTangentPointsX[cornerIndex][0] = bubblePointerVerticesX[cornerIndex] + interpolationFactor_B * deltaX_B; //--- Set tangent point X leaving
   bubblePointerTangentPointsY[cornerIndex][0] = bubblePointerVerticesY[cornerIndex] + interpolationFactor_B * deltaY_B; //--- Set tangent point Y leaving

   bubblePointerArcStartAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][1] - bubblePointerArcCentersY[cornerIndex], 
                                                          bubblePointerTangentPointsX[cornerIndex][1] - bubblePointerArcCentersX[cornerIndex]); //--- Set start angle
   bubblePointerArcEndAngles[cornerIndex] = MathArctan2(bubblePointerTangentPointsY[cornerIndex][0] - bubblePointerArcCentersY[cornerIndex], 
                                                        bubblePointerTangentPointsX[cornerIndex][0] - bubblePointerArcCentersX[cornerIndex]); //--- Set end angle
   
   for(int i = 0; i < 3; i++) {                                 //--- Loop over corners
      if(i == bubblePointerApexIndex) continue;                 //--- Skip apex corner
      
      bubblePointerTangentPointsX[i][0] = bubblePointerVerticesX[i]; //--- Set tangent X leaving to vertex
      bubblePointerTangentPointsY[i][0] = bubblePointerVerticesY[i]; //--- Set tangent Y leaving to vertex
      bubblePointerTangentPointsX[i][1] = bubblePointerVerticesX[i]; //--- Set tangent X arriving to vertex
      bubblePointerTangentPointsY[i][1] = bubblePointerVerticesY[i]; //--- Set tangent Y arriving to vertex
   }
}

Zunächst implementieren wir die Funktion PrecomputeBubbleGeometry, um das Layout für die Sprechblase festzulegen. Dabei weisen wir den Supersampling-Faktor einer lokalen Variablen zu, skalieren einen Basis-Offset für den Abstand und deklarieren anschließend eine Zentrierungsanpassung, um den Zeiger zu positionieren. Je nach bubblePointerOrientation legen wir die Hauptteilkoordinaten fest: Bei ORIENT_UP platzieren wir ihn unterhalb des Zeigers, wobei links, oben, rechts und unten anhand der skalierten Eingaben berechnet werden; berechnen die Zentrierung unter Anwendung des Offsets zur Ausrichtung, ermitteln die X-Mitte des Zeigers und definieren die Eckpunkte mit dem Scheitelpunkt oben in der Mitte sowie links und rechts an der Oberkante des Objekts, wobei bubblePointerApexIndex auf 0 gesetzt wird und der Basis-Start- und -Endpunkt von links nach rechts auf X festgelegt wird.

Bei ORIENT_DOWN positionieren wir den Hauptteil über dem Zeiger, verlegen die Spitze nach unten, passen die Reihenfolge der Scheitelpunkte für eine konsistente Umlaufrichtung der Eckpunkte an und aktualisieren den Start- und Endpunkt der Basis entsprechend; bei ORIENT_LEFT verschieben wir den Hauptteil rechts vom Zeiger, legen die Scheitelpunkte so fest, dass die Spitze links, die Unterseite und die Oberseite an der linken Seite des Hauptteils liegen, und verwenden die Y-Werte als Basis; bei ORIENT_RIGHT spiegeln wir den Hauptteil nach rechts und nehmen die entsprechenden Anpassungen vor. Diese bedingte Konfiguration gewährleistet eine an die Richtung angepasste Geometrie und ermöglicht so eine nahtlose Integration von Zeiger und Hauptteil ohne Überlappungen.

Um die Spitze des Zeigers abzurunden, rufen wir die Funktion ComputeBubbleTriangleRoundedCorners auf, skalieren den Radius des Scheitelpunkts, konzentrieren uns über bubblePointerApexIndex auf die Ecke an der Spitze, berechnen Kantenvektoren und Normalen wie bei der Vorberechnung des Dreiecks, bilden die Winkelhalbierende, berechnen den Sinus des Halbwinkels für den Mittenabstand, projizieren, um Tangenten zu finden, und legen Start- und Endwinkel mit der Funktion MathArctan2 fest. Schließlich legen wir bei den Ecken, die nicht an der Spitze liegen (Basis), die Tangenten direkt an den Eckpunkten fest und deaktivieren dort die Rundung, damit sie bündig mit dem Hauptteil verschmelzen – was entscheidend für ein sauberes Erscheinungsbild der Sprechblase ohne unnötige Kurven am Befestigungspunkt ist. Für das Zeichnen von Pixeln auf Basis von Bögen benötigen wir eine Hilfsfunktion.

bool BubbleAngleInArcSweep(int cornerIndex, double angle) {
   double twoPi = 2.0 * M_PI;                                   //--- Define two pi constant
   double startAngleMod = MathMod(bubblePointerArcStartAngles[cornerIndex] + twoPi, twoPi); //--- Modulo start angle
   double endAngleMod = MathMod(bubblePointerArcEndAngles[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 implementieren die Funktion BubbleAngleInArcSweep, um festzustellen, ob ein gegebener Winkel innerhalb des Bogenbereichs für eine bestimmte Ecke des Zeigers der Sprechblase liegt. Dazu definieren wir zunächst eine Konstante von 2 π und normalisieren den Start-, End- und Eingabewinkel mithilfe von MathMod, um sicherzustellen, dass sie im Bereich von 0 bis 2 π liegen, wobei wir eventuelle Umbrüche oder negative Werte berücksichtigen. Als Nächstes berechnen wir die Spanne gegen den Uhrzeigersinn zwischen dem normierten Start- und Endpunkt, und wenn diese π oder kleiner ist (was auf den kürzeren Bogen hindeutet), überprüfen wir den relativen Winkel vom Startpunkt aus anhand dieser Spanne mit einem kleinen Epsilon zur Genauigkeit; andernfalls berechnen wir die Spanne im Uhrzeigersinn als 2 π minus die Spanne gegen den Uhrzeigersinn und überprüfen den relativen Winkel vom Endpunkt aus. Diese Logik berücksichtigt beide Bogenrichtungen und gewährleistet so eine präzise Einbeziehung der Pixel beim Rendern, was für einen glatten, artefaktfreien, geschwungenen Rahmen des Sprechblasenzeigers in allen Ausrichtungen entscheidend ist. Um das Rechteck mit abgerundeten Ecken zu zeichnen, gehen wir wie folgt vor:

void FillBubbleRoundedRectangle(double left, double top, double width, double height, int radius, uint fillColor) {
   bubbleHighResCanvas.FillRectangle((int)(left + radius), (int)top, (int)(left + width - radius), (int)(top + height), fillColor); //--- Fill central rectangle
   bubbleHighResCanvas.FillRectangle((int)left, (int)(top + radius), (int)(left + radius), (int)(top + height - radius), fillColor); //--- Fill left strip
   bubbleHighResCanvas.FillRectangle((int)(left + width - radius), (int)(top + radius), (int)(left + width), (int)(top + height - radius), fillColor); //--- Fill right strip

   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + radius), radius, fillColor, 2); //--- Fill top-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + radius), radius, fillColor, 1); //--- Fill top-right quadrant
   FillBubbleCircleQuadrant((int)(left + radius), (int)(top + height - radius), radius, fillColor, 3); //--- Fill bottom-left quadrant
   FillBubbleCircleQuadrant((int)(left + width - radius), (int)(top + height - radius), radius, fillColor, 4); //--- Fill bottom-right quadrant
}

void FillBubbleCircleQuadrant(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
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, fillColor); //--- Set pixel
      }
   }
}

Hier implementieren wir die Funktion FillBubbleRoundedRectangle, um den gefüllten Hauptteil der Sprechblase als abgerundetes Rechteck auf der hochauflösenden Zeichenfläche darzustellen. Dabei wandeln wir die Koordinaten zur Pixelgenauigkeit in Ganzzahlen um, füllen den zentralen Bereich (mit Ausnahme der Ecken) mit FillRectangle und fügen anschließend linke und rechte vertikale Streifen hinzu, um die Seiten nahtlos abzudecken. Um die Abrundung abzuschließen, rufen wir für jede Ecke die Funktion FillBubbleCircleQuadrant auf und passen dabei die quadrantenspezifische Logik so an, dass nur die Pixel des jeweiligen Viertelkreises ausgefüllt werden.

In FillBubbleCircleQuadrant wandeln wir den Radius aus Gründen der Genauigkeit in einen Double-Wert um, durchlaufen zur Kantenglättung einen erweiterten Delta-Bereich, prüfen anhand der Delta-Vorzeichen die Zugehörigkeit zu einem Quadranten (z. B. Quadrant 1 bei positivem X-Wert und negativem Y-Wert), berechnen den Abstand mit MathSqrt und setzen die Pixel innerhalb des Radius mit PixelSet, um glatte Kurven zu gewährleisten, die sich nahtlos in die Rechteckstreifen einfügen. Diese modulare Füllung ist wichtig, um die Konsistenz über alle Ausrichtungen hinweg zu gewährleisten, da sie die Rechtecklogik wiederverwendet und gleichzeitig die Anbindung von Zeigern ohne Fülllücken ermöglicht. Das Ausfüllen des Rechteckrahmens ist einfach und unkompliziert. Definieren wir nun eine Logik, um ein abgerundetes Dreieck für den Zeiger auszufüllen.

void FillBubbleRoundedTriangle(uint fillColor) {
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      double minX = bubblePointerVerticesX[0], maxX = bubblePointerVerticesX[0]; //--- Initialize min and max X
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesX[i] < minX) minX = bubblePointerVerticesX[i]; //--- Update min X
         if(bubblePointerVerticesX[i] > maxX) maxX = bubblePointerVerticesX[i]; //--- Update max X
      }

      int xStart = (int)MathCeil(minX);                         //--- Compute start X
      int xEnd   = (int)MathFloor(maxX);                        //--- Compute end X

      for(int x = xStart; x <= xEnd; x++) {                     //--- Loop over scanlines
         double scanlineX = (double)x + 0.5;                    //--- Set scanline X position
         double yIntersections[12];                             //--- Declare intersections array
         int intersectionCount = 0;                             //--- Initialize intersection count

         for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {   //--- Loop over edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index

            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges

            double startX, startY, endX, endY;                  //--- Declare edge coordinates

            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX   = bubblePointerVerticesX[nextIndex];     //--- Set end X to next vertex
               endY   = bubblePointerVerticesY[nextIndex];     //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX   = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY   = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
            }

            double edgeMinX = (startX < endX) ? startX : endX;  //--- Compute edge min X
            double edgeMaxX = (startX > endX) ? startX : endX;  //--- Compute edge max X

            if(scanlineX < edgeMinX || scanlineX > edgeMaxX) continue; //--- Skip if outside edge X
            if(MathAbs(endX - startX) < 1e-12) continue;        //--- Skip if vertical edge

            double interpolationFactor = (scanlineX - startX) / (endX - startX); //--- Compute factor
            if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue; //--- Skip if outside segment

            yIntersections[intersectionCount++] = startY + interpolationFactor * (endY - startY); //--- Add intersection Y
         }

         {                                                      //--- Intersect apex arc block
            int cornerIndex = bubblePointerApexIndex;           //--- Set corner index
            double centerX  = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
            double centerY  = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
            double radius   = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
            double deltaX   = scanlineX - centerX;              //--- Compute delta X

            if(MathAbs(deltaX) <= radius) {                     //--- Check if within radius
               double deltaY = MathSqrt(radius * radius - deltaX * deltaX); //--- Compute delta Y

               double candidates[2];                            //--- Declare candidates array
               candidates[0] = centerY - deltaY;                //--- Set top candidate
               candidates[1] = centerY + deltaY;                //--- Set bottom candidate

               for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) { //--- Loop over candidates
                  double angle = MathArctan2(candidates[candidateIndex] - centerY, scanlineX - centerX); //--- Compute angle
                  if(BubbleAngleInArcSweep(cornerIndex, angle)) //--- Check if in arc sweep
                     yIntersections[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(yIntersections[a] > yIntersections[b]) {       //--- Check if swap needed
                  double temp = yIntersections[a];               //--- Temporary store
                  yIntersections[a] = yIntersections[b];         //--- Swap values
                  yIntersections[b] = temp;                      //--- Complete swap
               }

         for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) { //--- Loop over pairs
            int yTop    = (int)MathCeil(yIntersections[pairIndex]); //--- Compute top Y
            int yBottom = (int)MathFloor(yIntersections[pairIndex + 1]); //--- Compute bottom Y
            for(int y = yTop; y <= yBottom; y++)                //--- Loop over vertical span
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }

   } else {                                                     //--- Handle vertical orientation
      double minY = bubblePointerVerticesY[0], maxY = bubblePointerVerticesY[0]; //--- Initialize min and max Y
      for(int i = 1; i < 3; i++) {                              //--- Loop over vertices
         if(bubblePointerVerticesY[i] < minY) minY = bubblePointerVerticesY[i]; //--- Update min Y
         if(bubblePointerVerticesY[i] > maxY) maxY = bubblePointerVerticesY[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 edges
            int nextIndex = (edgeIndex + 1) % 3;                //--- Get next index
            
            if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
            
            double startX, startY, endX, endY;                  //--- Declare edge coordinates
            
            if(edgeIndex == bubblePointerApexIndex) {           //--- Check if from apex
               startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
               startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
               endX = bubblePointerVerticesX[nextIndex];       //--- Set end X to next vertex
               endY = bubblePointerVerticesY[nextIndex];       //--- Set end Y to next vertex
            } else {                                            //--- Handle to apex
               startX = bubblePointerVerticesX[edgeIndex];     //--- Set start X from vertex
               startY = bubblePointerVerticesY[edgeIndex];     //--- Set start Y from vertex
               endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
               endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to 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
         }

         int cornerIndex = bubblePointerApexIndex;              //--- Set corner index
         double centerX = bubblePointerArcCentersX[cornerIndex]; //--- Get center X
         double centerY = bubblePointerArcCentersY[cornerIndex]; //--- Get center Y
         double radius = (double)bubblePointerApexRadiusPixels * supersamplingFactor; //--- Get scaled radius
         double deltaY = scanlineY - centerY;                   //--- Compute delta Y

         if(MathAbs(deltaY) <= radius) {                        //--- Check if within 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(BubbleAngleInArcSweep(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
               bubbleHighResCanvas.PixelSet(x, y, fillColor);   //--- Set pixel with fill color
         }
      }
   }
}

Wir implementieren die Funktion FillBubbleRoundedTriangle, um den dreieckigen Zeiger der Sprechblase auf der hochauflösenden Zeichenfläche auszufüllen, wobei wir die Scanlinienrichtung je nach Ausrichtung anpassen, um eine optimale Effizienz zu erzielen – dabei verwenden wir horizontale Scans (x-Schleife), wenn bubbleIsHorizontalOrientation für linke/rechte Zeiger wahr ist, andernfalls vertikale Scans (y-Schleife). Bei horizontaler Ausrichtung ermitteln wir die Minimal- und Maximalwerte für X anhand der Eckpunkte des Zeigers, durchlaufen die Ganzzahl x mit einem Versatz von einem halben Pixel, erfassen per Interpolation ausschließlich y-Schnittpunkte von an die Scheitelpunkte angrenzenden Kanten (wobei die Basis übersprungen wird), fügen Bogenkandidaten hinzu, indem wir die Kreisgleichung für deltaX lösen, sofern sie innerhalb des Radius liegen und durch BubbleAngleInArcSweep validiert wurden, sortieren mit dem Bubble-Sort-Verfahren und füllen vertikale Bereiche zwischen jeweiligen Schnittpunktpaaren mithilfe von PixelSet mit fillColor auf.

Bei vertikalen Fällen spiegeln wir den Vorgang mit Min/Max Y, einer y-Schleife auf der Scanline Y, x-Schnittpunkten von Kanten und Bögen (unter Verwendung von deltaY für Kandidaten), Sortierung und horizontalen Füllungen, um eine lückenlose Abdeckung zu gewährleisten. Nachdem die Füllung für den rechteckigen Teil und den dreieckigen Zeiger der Sprechblase fertiggestellt ist, definieren wir nun die Logik für den Rahmen, der die Sprechblase umschließt. Um dies zu erreichen, verfolgen wir den folgenden Ansatz.

void DrawBubbleBorder(uint borderColorARGB) {
   int scaledThickness = bubbleBorderThicknessPixels * supersamplingFactor; //--- Scale border thickness
   int scaledBodyRadius = bubbleBodyCornerRadiusPixels * supersamplingFactor; //--- Scale body radius

   if(bubblePointerOrientation == ORIENT_UP || bubblePointerOrientation == ORIENT_DOWN) { //--- Check for up or down orientation
      if(bubblePointerOrientation == ORIENT_DOWN) {             //--- Check for down orientation
         DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      } else {                                                  //--- Handle up orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubblePointerBaseStart, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw left segment of top edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubblePointerBaseEnd, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw right segment of top edge
      }

      if(bubblePointerOrientation == ORIENT_UP) {               //--- Check for up orientation
         DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge
      } else {                                                  //--- Handle down orientation
         if(bubblePointerBaseStart > bubbleBodyLeft + scaledBodyRadius) //--- Check if base start exceeds left radius
            DrawBubbleHorizontalEdge(bubblePointerBaseStart, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw left segment of bottom edge
         if(bubblePointerBaseEnd < bubbleBodyRight - scaledBodyRadius) //--- Check if base end below right radius
            DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubblePointerBaseEnd, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw right segment of bottom edge
      }

      DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge

   } else {                                                     //--- Handle left or right orientation
      DrawBubbleHorizontalEdge(bubbleBodyLeft + scaledBodyRadius, bubbleBodyTop, bubbleBodyRight - scaledBodyRadius, bubbleBodyTop, scaledThickness, borderColorARGB); //--- Draw top edge
      DrawBubbleHorizontalEdge(bubbleBodyRight - scaledBodyRadius, bubbleBodyBottom, bubbleBodyLeft + scaledBodyRadius, bubbleBodyBottom, scaledThickness, borderColorARGB); //--- Draw bottom edge

      if(bubblePointerOrientation == ORIENT_RIGHT) {            //--- Check for right orientation
         DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw left edge
      } else {                                                  //--- Handle left orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubblePointerBaseStart, bubbleBodyLeft, bubbleBodyTop + scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw top segment of left edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyLeft, bubbleBodyBottom - scaledBodyRadius, bubbleBodyLeft, bubblePointerBaseEnd, scaledThickness, borderColorARGB); //--- Draw bottom segment of left edge
      }

      if(bubblePointerOrientation == ORIENT_LEFT) {             //--- Check for left orientation
         DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw right edge
      } else {                                                  //--- Handle right orientation
         if(bubblePointerBaseStart > bubbleBodyTop + scaledBodyRadius) //--- Check if base start exceeds top radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubbleBodyTop + scaledBodyRadius, bubbleBodyRight, bubblePointerBaseStart, scaledThickness, borderColorARGB); //--- Draw top segment of right edge
         if(bubblePointerBaseEnd < bubbleBodyBottom - scaledBodyRadius) //--- Check if base end below bottom radius
            DrawBubbleVerticalEdge(bubbleBodyRight, bubblePointerBaseEnd, bubbleBodyRight, bubbleBodyBottom - scaledBodyRadius, scaledThickness, borderColorARGB); //--- Draw bottom segment of right edge
      }
   }

   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI, M_PI * 1.5); //--- Draw top-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyTop + scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 1.5, M_PI * 2.0); //--- Draw top-right arc
   DrawBubbleCornerArc((int)(bubbleBodyLeft + scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, M_PI * 0.5, M_PI); //--- Draw bottom-left arc
   DrawBubbleCornerArc((int)(bubbleBodyRight - scaledBodyRadius), (int)(bubbleBodyBottom - scaledBodyRadius), scaledBodyRadius, scaledThickness, borderColorARGB, 0.0, M_PI * 0.5); //--- Draw bottom-right arc

   for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {         //--- Loop over pointer edges
      int nextIndex = (edgeIndex + 1) % 3;                      //--- Get next index
      
      if(edgeIndex != bubblePointerApexIndex && nextIndex != bubblePointerApexIndex) continue; //--- Skip non-apex edges
      
      double startX, startY, endX, endY;                        //--- Declare edge coordinates
      
      if(edgeIndex == bubblePointerApexIndex) {                 //--- Check if from apex
         startX = bubblePointerTangentPointsX[bubblePointerApexIndex][0]; //--- Set start X from tangent
         startY = bubblePointerTangentPointsY[bubblePointerApexIndex][0]; //--- Set start Y from tangent
         endX = bubblePointerVerticesX[nextIndex];              //--- Set end X to next vertex
         endY = bubblePointerVerticesY[nextIndex];              //--- Set end Y to next vertex
      } else {                                                  //--- Handle to apex
         startX = bubblePointerVerticesX[edgeIndex];            //--- Set start X from vertex
         startY = bubblePointerVerticesY[edgeIndex];            //--- Set start Y from vertex
         endX = bubblePointerTangentPointsX[bubblePointerApexIndex][1]; //--- Set end X to tangent
         endY = bubblePointerTangentPointsY[bubblePointerApexIndex][1]; //--- Set end Y to tangent
      }

      DrawBubbleStraightEdge(startX, startY, endX, endY, scaledThickness, borderColorARGB); //--- Draw straight edge
   }

   DrawBubbleTriangleCornerArc(bubblePointerApexIndex, scaledThickness, borderColorARGB); //--- Draw apex arc
}

void DrawBubbleHorizontalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw horizontal edge using straight edge
}

void DrawBubbleVerticalEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   DrawBubbleStraightEdge(startX, startY, endX, endY, thickness, edgeColor); //--- Draw vertical edge using straight edge
}

void DrawBubbleStraightEdge(double startX, double startY, double endX, double endY, int thickness, uint edgeColor) {
   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 = borderExtensionMultiplier * (double)thickness; //--- 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(bubbleHighResCanvas, verticesX, verticesY, edgeColor); //--- Fill quadrilateral for edge
}

void DrawBubbleCornerArc(int centerX, int centerY, int radius, int thickness, uint edgeColor,
                         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
            bubbleHighResCanvas.PixelSet(centerX + deltaX, centerY + deltaY, edgeColor); //--- Set pixel
      }
   }
}

void DrawBubbleTriangleCornerArc(int cornerIndex, int thickness, uint edgeColor) {
   double centerX = bubblePointerArcCentersX[cornerIndex];      //--- Get center X
   double centerY = bubblePointerArcCentersY[cornerIndex];      //--- Get center Y
   double radius = (double)bubblePointerApexRadiusPixels * 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(BubbleAngleInArcSweep(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 < bubbleHighResCanvas.Width() && pixelY >= 0 && pixelY < bubbleHighResCanvas.Height()) //--- Check if within bounds
               bubbleHighResCanvas.PixelSet(pixelX, pixelY, edgeColor); //--- Set pixel
         }
      }
   }
}

Hier implementieren wir die Funktion DrawBubbleBorder, um den vollständigen Rand der Sprechblase auf der hochauflösenden Zeichenfläche darzustellen, wobei wir die Dicke und den Radius des Hauptteils mittels Supersampling skalieren und anschließend je nach Ausrichtung bedingt segmentierte Kanten zeichnen – für oben/unten werden die oberen und unteren horizontalen Kanten mit Unterteilungen um die Zeigerbasis herum behandelt, sofern diese vorhanden ist, sowie vollständige vertikale Kanten; Bei links/rechts verfahren wir analog mit vollständigen horizontalen Linien und segmentierten vertikalen Linien, wobei wir DrawBubbleHorizontalEdge und DrawBubbleVerticalEdge verwenden, um ein Überzeichnen der Zeigerverbindung zu vermeiden. Als Nächstes zeichnen wir die vier Eckbögen des Hauptteils mit DrawBubbleCornerArc unter Verwendung vordefinierter Winkelbereiche in Radianten, durchlaufen die drei Zeigerkanten in einer Schleife (mit Schwerpunkt auf den an den Scheitelpunkt angrenzenden Kanten), um mit DrawBubbleStraightEdge gerade Kanten aus den Tangenten hinzuzufügen, und vervollständigen das Ganze mit dem Scheitelpunktbogen mittels DrawBubbleTriangleCornerArc.

Um das Zeichnen von Kanten zu vereinheitlichen, rufen Sie DrawBubbleHorizontalEdge und DrawBubbleVerticalEdge einfach DrawBubbleStraightEdge auf, das Richtungs- und Senkrechtvektoren berechnet, die Endpunkte um das borderExtensionMultiplier-fache der Strichstärke verlängert, um glatte Übergänge zu erzielen, einen viereckigen Streifen bildet und diesen mit FillQuadrilateral in der Kantenfarbe ausfüllt. In DrawBubbleCornerArc erzeugen wir den gekrümmten Randring, indem wir den Innen- und Außenradius anhand der halben Dicke berechnen, ein erweitertes Pixelraster durchlaufen und Pixel setzen, sofern sie innerhalb des Abstands liegen. Die Funktion IsAngleBetween überprüft das Bogensegment und gewährleistet so eine präzise Krümmung. Schließlich spiegelt DrawBubbleTriangleCornerArc dies für die Zeigerspitze wider, wobei ein skalierter Radius, Pixelschleifen mit MathSqrt für den Abstand, MathArctan2 für den Winkel, BubbleAngleInArcSweep für die Einbeziehung und MathRound für die Koordinaten mit Bereichsprüfungen vor dem Aufruf von PixelSet verwendet werden, wodurch kantenglättete Rahmen entstehen, die nahtlos in den Hauptteil übergehen. Wir können diese Logik nun kombinieren, um die Sprechblase zu erzeugen.

void DrawBubble() {
   uint backgroundColorARGB = ColorToARGBWithOpacity(bubbleBackgroundColor, bubbleBackgroundOpacityPercent); //--- Get background ARGB
   uint borderColorARGB = ColorToARGBWithOpacity(bubbleBorderColor, bubbleBorderOpacityPercent); //--- Get border ARGB

   FillBubble(backgroundColorARGB);                             //--- Fill bubble

   if(bubbleShowBorder && bubbleBorderThicknessPixels > 0)      //--- Check if border should be shown
      DrawBubbleBorder(borderColorARGB);                        //--- Draw bubble border

   BicubicDownsample(bubbleCanvas, bubbleHighResCanvas);        //--- Downsample to display canvas

   bubbleCanvas.FontSet("Arial", 13, FW_NORMAL);                //--- Set font for text
   string displayText = "Bubble";                               //--- Set display text
   int textWidth, textHeight;                                   //--- Declare text dimensions
   bubbleCanvas.TextSize(displayText, textWidth, textHeight);   //--- Get text size
   
   int bodyDisplayLeft = (int)(bubbleBodyLeft / supersamplingFactor); //--- Compute display left
   int bodyDisplayTop = (int)(bubbleBodyTop / supersamplingFactor); //--- Compute display top
   int bodyDisplayWidth = (int)((bubbleBodyRight - bubbleBodyLeft) / supersamplingFactor); //--- Compute display width
   int bodyDisplayHeight = (int)((bubbleBodyBottom - bubbleBodyTop) / supersamplingFactor); //--- Compute display height
   
   int textPositionX = bodyDisplayLeft + (bodyDisplayWidth - textWidth) / 2; //--- Compute text X position
   int textPositionY = bodyDisplayTop + (bodyDisplayHeight - textHeight) / 2; //--- Compute text Y position
   bubbleCanvas.TextOut(textPositionX, textPositionY, displayText, (uint)0xFF000000, TA_LEFT); //--- Draw text on canvas
}

void FillBubble(uint fillColor) {
   FillBubbleRoundedRectangle(bubbleBodyLeft, bubbleBodyTop, bubbleBodyRight - bubbleBodyLeft, bubbleBodyBottom - bubbleBodyTop, 
                              bubbleBodyCornerRadiusPixels * supersamplingFactor, fillColor); //--- Fill bubble body rectangle

   FillBubbleRoundedTriangle(fillColor);                        //--- Fill bubble pointer triangle
}

Um die Zeichenlogik zusammenzufassen, definieren wir die Funktion DrawBubble, die die Darstellung der Sprechblase auf der Zeichenfläche koordiniert. Zunächst werden die Hintergrund- und Rahmenfarben mithilfe von ColorToARGBWithOpacity in ARGB-Werte mit Deckkraft umgewandelt, um durchscheinende Effekte zu erzielen. Um die Form zu erstellen, rufen wir FillBubble mit der Hintergrundfarbe auf, um die hochauflösende Zeichenfläche zu füllen, und wenn Rahmen aktiviert sind, rufen wir DrawBubbleBorder auf, um den Umriss hinzuzufügen. Anschließend skalieren wir mit BicubicDownsample auf die Standard-Canvas herunter, um eine kantenglättete Ausgabe zu erhalten. Abschließend legen wir auf der Standard-Zeichenfläche die Schriftart Arial in Schriftgröße 13 mit FW_NORMAL fest, messen die Beschriftung Bubble aus und zentrieren sie innerhalb der auf Anzeigekoordinaten skalierten Begrenzungen des Bildbereichs und zeichnen die Beschriftung mit TextOut in durchgehendem Schwarz linksbündig.

In der Funktion FillBubble füllen wir den Hauptteil mit FillBubbleRoundedRectangle unter Verwendung vorberechneter Koordinaten und eines skalierten Radius und fügen anschließend die Zeigerfüllung über FillBubbleRoundedTriangle hinzu, wodurch die Formen bei hoher Auflösung nahtlos zu einer einheitlichen Blase kombiniert werden. Wir können diese Funktion nun auch in der Initialisierung aufrufen, um die Blase zu zeichnen.

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   
   // added this for the bubble in initialization alongside the other prior shapes
   
   bubbleIsHorizontalOrientation = (bubblePointerOrientation == ORIENT_LEFT || bubblePointerOrientation == ORIENT_RIGHT); //--- Determine if orientation is horizontal

   int bubbleCanvasWidth, bubbleCanvasHeight;                   //--- Declare bubble canvas dimensions
   if(bubbleIsHorizontalOrientation) {                          //--- Check for horizontal orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + bubblePointerHeightPixels + 40; //--- Compute width for horizontal
      bubbleCanvasHeight = bubbleBodyHeightPixels + 40;         //--- Compute height for horizontal
   } else {                                                     //--- Handle vertical orientation
      bubbleCanvasWidth = bubbleBodyWidthPixels + 40;           //--- Compute width for vertical
      bubbleCanvasHeight = bubbleBodyHeightPixels + bubblePointerHeightPixels + 40; //--- Compute height for vertical
   }

   int bubblePositionY = trianglePositionY + triangleCanvasHeight + shapesGapPixels; //--- Set bubble Y position below triangle

   if(!bubbleCanvas.CreateBitmapLabel(0, 0, bubbleCanvasName, shapesPositionX, bubblePositionY,
                                    bubbleCanvasWidth, bubbleCanvasHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create bubble canvas bitmap label
      Print("Error creating bubble canvas: ", GetLastError());  //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   if(!bubbleHighResCanvas.Create(bubbleCanvasName + "_hires",
                        bubbleCanvasWidth * supersamplingFactor,
                        bubbleCanvasHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create high-res bubble canvas
      Print("Error creating bubble hi-res canvas: ", GetLastError()); //--- Print error message if creation fails
      return(INIT_FAILED);                                      //--- Return initialization failure
   }

   bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));                 //--- Clear bubble canvas
   bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));          //--- Clear high-res bubble canvas

   PrecomputeBubbleGeometry();                                  //--- Precompute bubble geometry
   
   DrawBubble();                                                //--- Draw bubble

   bubbleCanvas.Update();                                       //--- Update bubble canvas display

   return(INIT_SUCCEEDED);                                      //--- Return initialization success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   rectangleHighResCanvas.Destroy();                            //--- Destroy high-res rectangle canvas
   rectangleCanvas.Destroy();                                   //--- Destroy rectangle canvas
   ObjectDelete(0, rectangleCanvasName);                        //--- Delete rectangle canvas object

   triangleHighResCanvas.Destroy();                             //--- Destroy high-res triangle canvas
   triangleCanvas.Destroy();                                    //--- Destroy triangle canvas
   ObjectDelete(0, triangleCanvasName);                         //--- Delete triangle canvas object

   bubbleHighResCanvas.Destroy();                               //--- Destroy high-res bubble canvas
   bubbleCanvas.Destroy();                                      //--- Destroy bubble canvas
   ObjectDelete(0, bubbleCanvasName);                           //--- Delete bubble 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

      bubbleCanvas.Erase(ColorToARGB(clrNONE, 0));              //--- Clear bubble canvas
      bubbleHighResCanvas.Erase(ColorToARGB(clrNONE, 0));       //--- Clear high-res bubble canvas
      DrawBubble();                                             //--- Redraw bubble
      bubbleCanvas.Update();                                    //--- Update bubble canvas display
   }
}

Wir erweitern den OnInit-Ereignishandler, um die Sprechblase neben den bisherigen Formen einzubinden. Dabei wird anhand der Zeiger für links und rechts mit bubbleIsHorizontalOrientation ermittelt, ob die Ausrichtung horizontal ist, und anschließend werden die Canvas-Abmessungen entsprechend berechnet – wobei bei horizontaler Ausrichtung die Zeigerhöhe zur Breite und bei vertikaler Ausrichtung zur Höhe hinzugefügt wird, einschließlich des Abstands. Als Nächstes positionieren wir die Blase unterhalb des Dreiecks, wobei wir den Y-Wert der vorherigen Zeichenfläche plus den Abstand verwenden, erstellen die Standard-Zeichenfläche über CreateBitmapLabel mit ARGB-Normalisierung für die Transparenz und die hochauflösende Version mit Create unter Verwendung skalierter Größen, protokollieren Fehler und brechen die Initialisierung ab, falls sie fehlschlägt. Zur Vorbereitung des Renderings löschen wir beide Bereiche mit transparentem ARGB, rufen PrecomputeBubbleGeometry für das Layout auf, zeichnen mit DrawBubble, aktualisieren die Anzeige und geben INIT_SUCCEEDED zurück.

In OnDeinit erweitern wir die Bereinigung, indem wir die Zeichenfläche für hochauflösende und Standard-Blasen zerstören, das Objekt löschen und das Diagramm neu zeichnen. Zur besseren Übersichtlichkeit haben wir die jeweiligen Erweiterungen hervorgehoben.

Schließlich fügen wir in OnChartEvent für CHARTEVENT_CHART_CHANGE eine Blasenverarbeitung hinzu, indem wir die Blasen löschen, mit DrawBubble neu zeichnen und sie zusammen mit Rechtecken und Dreiecken aktualisieren, um eine reaktionsschnelle Anpassung an Änderungen am Diagramm zu gewährleisten. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

ABGERUNDETE SPRECHBLASE

Auf dem Bild ist zu erkennen, dass die Blase gemäß unseren Vorgaben korrekt dargestellt wird. Nun haben wir eine weitere Änderung vorgenommen, um die Länge der Randverlängerung zu steuern: Wir haben die feste Länge abgeschafft und stattdessen ein Längenverhältnis eingeführt, um eine dynamischere Steuerung zu ermöglichen. Wir hielten das für besser, da bei einer Vergrößerung der Rahmenbreite der Übergang zwischen dem Zeiger und dem Rahmen des Rechtecks nicht nahtlos war. Aus Gründen der Einheitlichkeit haben wir die Randverlängerung in den Funktionen DrawRectStraightEdge und DrawTriStraightEdge wie folgt angepasst.

double extensionLength = borderExtensionMultiplier * (double)thickness; //--- Set extension length

Beachten Sie die Auswirkungen, die das Update mit sich gebracht hat, insbesondere bei den Rahmen der Blasen.

VERGLEICH DER VERLÄNGERUNGSLÄNGEN

Anhand der Visualisierung lässt sich erkennen, wie sich das Erweiterungsverhältnis auf die Blasenverbindungen auswirkt. Das ist besonders wichtig, wenn Sie Formen mit großer Breite verwenden. Das war die einfachste Möglichkeit, das Verbindungsproblem zu lösen, indem wir die Erweiterung dynamisch proportional skalierten. Wenn Sie einen komplexeren, sanfteren Ansatz wünschen, können Sie den Ansatz mit Übergangskurven in Betracht ziehen, wie sie etwa bei Bezier-Kurven verwendet werden. Das habe ich im Fach Straßenverkehrstechnik gelernt, und kurz gesagt sieht es so aus.

BEZIER-KURVEN

Es ist wichtig zu beachten, dass es verschiedene Arten von Bézier-Kurven gibt, und ich würde quadratische bzw. kubische Bézier-Kurven empfehlen, falls Sie diesen Ansatz tatsächlich in Betracht ziehen, oder die kubische Methode, wenn Sie mutig genug sind. Wir halten uns jedoch einfach an die einfache Erweiterungsmethode. Insgesamt lässt sich feststellen, dass wir eine Sprechblase erstellt und damit unsere Ziele erreicht haben. Nun bleibt nur noch, die Funktionsfähigkeit des Systems zu testen, was im folgenden Abschnitt behandelt wurde.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung als einzelnes GIF-Bild.

BACKTEST-GIF


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir in MQL5 abgerundete Sprechblasen erstellt haben, indem wir vektorbasierte Rechtecke und Dreiecke kombiniert und die Ausrichtung der Zeiger nach oben, unten, links oder rechts gesteuert haben. Wir haben Supersampling zur Kantenglättung integriert und bieten zudem anpassbare Abmessungen, Radien, Rahmen und Deckkraftwerte, um dynamische UI-Elemente zu erstellen. Mit diesen abgerundeten Sprechblasen und Ausrichtungsfunktionen sind wir bestens gerüstet, um ansprechende grafische Komponenten zu entwickeln, die sich direkt für fortgeschrittene Anwendungen in unseren zukünftigen Trading-Tools einsetzen lassen. Viel Spaß beim Handeln!

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

Der MQL5-Standardbibliotheks-Explorer (Teil 7): Interaktive Positionskennzeichnung mit CCanvas Der MQL5-Standardbibliotheks-Explorer (Teil 7): Interaktive Positionskennzeichnung mit CCanvas
In diesem Artikel zeigen wir, wie man mithilfe von CCanvas aus der MQL5-Standardbibliothek ein Tool zur Visualisierung von Positionsdaten erstellt. Dieses Projekt vertieft Ihre Kenntnisse im Umgang mit Bibliotheksmodulen und bietet Händlern gleichzeitig ein praktisches Werkzeug, um offene Positionen direkt in einem Live-Chart zu visualisieren und mit ihnen zu interagieren. Nehmen Sie an der Diskussion teil, um mehr zu erfahren.
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.
Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 59): Einsatz geometrischer Asymmetrie zur Erkennung präziser Ausbrüche aus fraktalen Konsolidierungsphasen Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 59): Einsatz geometrischer Asymmetrie zur Erkennung präziser Ausbrüche aus fraktalen Konsolidierungsphasen
Bei der Untersuchung einer Vielzahl von Breakout-Setups ist mir aufgefallen, dass gescheiterte Breakouts selten auf mangelnde Volatilität zurückzuführen waren, sondern häufiger auf eine schwache interne Struktur. Diese Beobachtung führte zu dem in diesem Artikel vorgestellten Rahmenkonzept. Der Ansatz identifiziert Muster, bei denen das letzte Kurssegment eine überdurchschnittliche Länge, Steilheit und Geschwindigkeit aufweist – eindeutige Anzeichen für den Aufbau von Momentum im Vorfeld einer gerichteten Ausweitung. Indem sie diese subtilen geometrischen Ungleichgewichte innerhalb der Konsolidierungsphase erkennen, können Trader Ausbrüche mit höherer Wahrscheinlichkeit antizipieren, bevor der Kurs die Konsolidierungsspanne verlässt. Lesen Sie weiter, um zu erfahren, wie dieses auf Fraktalen basierende geometrische Modell strukturelle Ungleichgewichte in präzise Ausbruchsignale umsetzt.
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.