English Русский
preview
MQL5 Trading Tools (Teil 16): Verbessertes Supersampling-Anti-Aliasing (SSAA) und hochauflösendes Rendering

MQL5 Trading Tools (Teil 16): Verbessertes Supersampling-Anti-Aliasing (SSAA) und hochauflösendes Rendering

MetaTrader 5Handelssysteme |
16 0
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

In unserem vorherigen Artikel (Teil 15) haben wir ein Zeichenflächen-Dashboard in MetaQuotes Language 5 (MQL5) entwickelt, das Unschärfeeffekte, Schattenrendering und flüssiges Scrollen mit dem Mausrad für die interaktive Marktbeobachtung umfasst und über visuelle Panels sowie anpassbare Designs verfügt. In Teil 16 erweitern wir das Dashboard um die Techniken des Anti-Aliasing und hochauflösendes Rendering. Dabei nutzen wir Supersampling, um glattere Grafiken, Rahmen und Elemente zu erzielen. Diese Erweiterung umfasst hochauflösende Zeichenflächen (Canvas) für Statistik- und Textpanels, präzise Zeichenfunktionen für abgerundete Formen sowie eine optimierte Interaktivität für eine bessere visuelle Qualität. Wir werden die folgenden Themen behandeln:

  1. Verstehen von Anti-Aliasing und hochauflösenden Rendering-Techniken
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende verfügen Sie über ein aktualisiertes MQL5-Dashboard mit verbesserter Darstellung für übersichtlichere und professionellere Marktvisualisierungen – legen wir los!


Verstehen von Anti-Aliasing und hochauflösenden Rendering-Techniken

Das Framework für Anti-Aliasing und hochauflösendes Rendering behandelt visuelle Artefakte in der digitalen Grafik, wie beispielsweise gezackte Kanten oder „Aliasing“, die auftreten, wenn kontinuierliche Formen auf einem diskreten Pixelraster dargestellt werden. Aliasing äußert sich in Form von Treppenmustern an Linien, Kurven oder Rändern und beeinträchtigt dadurch die Übersichtlichkeit und Professionalität von Darstellungen wie unseren Handels-Dashboards. Um dies zu kompensieren, werden bei Techniken wie Supersampling Szenen zunächst mit einer höheren Auflösung – in der Regel einem Vielfachen der Zielgröße – gerendert und anschließend durch Mittelung der Pixel heruntergerechnet. Dadurch wird eine Glättung der Kanten durch Farbüberblendung und Erzeugung eines natürlicheren Erscheinungsbildes erreicht. Nachfolgend finden Sie ein Beispiel für den Supersampling-Prozess.

SUPER-SAMPLING

Andererseits ergänzt das hochauflösende Rendering das Anti-Aliasing, indem größere Zeichenflächen genutzt werden, um feinere Details vor der Verkleinerung zu erfassen, wodurch die Gesamtbildqualität verbessert wird, ohne die endgültige Ausgabegröße zu erhöhen. Dies ist insbesondere bei MQL5-Anwendungen von Bedeutung, bei denen eine präzise Darstellung von Marktdaten, wie beispielsweise Diagrammen und Statistiken, das Verständnis der Nutzer verbessert und deren Entscheidungsfindung unterstützt, indem Verzerrungen minimiert werden. So berechnet beispielsweise die bikubische Interpolation, ein fortgeschrittenes Resampling-Verfahren, neue Pixelwerte anhand eines gewichteten Durchschnitts der umgebenden Pixel, wodurch die Glätte bei Skalierungsvorgängen erhalten bleibt und so zu besser kantengeglätteten Ergebnissen beiträgt.

Wir planen, Supersampling mit einem Faktor von 4 für Statistiken und Textpanels zu integrieren und Elemente auf hochauflösenden Zeichenflächen zu zeichnen, wobei wir zur Gewährleistung der Präzision mathematische Funktionen für Bögen, Vierecke und abgerundete Dreiecke verwenden. Anschließend führen wir ein Herunterskalieren mittels Pixelmittelung durch, um kantengeglättete Rahmen, Pfeile und Formen zu erzielen und dabei insbesondere bei Bildlaufleistenelementen und Statistik-Header-Boxen eine effiziente Darstellung beizubehalten. Kurz gesagt: Hier ist eine visuelle Darstellung unserer Ziele.

ZIELE DER WEITERENTWICKLUNG


Implementierung in MQL5

Um das Programm in MQL5 zu erweitern, müssen wir neue Definitionen, globale Variablen und Eingabeparameter hinzufügen, um die neuen Erweiterungen für das hochauflösende Rendering und die Supersampling-Funktionen wie folgt zu steuern.

//+------------------------------------------------------------------+
//|                                       Canvas Dashboard PART4.mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link "https://t.me/Forex_Algo_Trader"
#property version "1.00"
#property strict

// Added two new canvas objects for high-resolution rendering
//+------------------------------------------------------------------+
//| Canvas objects                                                   |
//+------------------------------------------------------------------+
CCanvas canvasGraph;                    //--- Declare graph canvas object
CCanvas canvasStats;                    //--- Declare stats canvas object
CCanvas canvasStatsHighRes;             //--- Declare stats high-res canvas object
CCanvas canvasHeader;                   //--- Declare header canvas object
CCanvas canvasText;                     //--- Declare text canvas object
CCanvas canvasTextHighRes;              //--- Declare text high-res canvas object

// Added names for the new high-resolution canvases
//+------------------------------------------------------------------+
//| Canvas names                                                     |
//+------------------------------------------------------------------+
string canvasGraphName = "GraphCanvas"; //--- Set graph canvas name
string canvasStatsName = "StatsCanvas"; //--- Set stats canvas name
string canvasStatsHighResName = "StatsCanvasHighRes"; //--- Set stats high-res name
string canvasHeaderName = "HeaderCanvas"; //--- Set header canvas name
string canvasTextName = "TextCanvas";   //--- Set text canvas name
string canvasTextHighResName = "TextCanvasHighRes"; //--- Set text high-res name

// New group
sinput group "=== TEXT PANEL SETTINGS ==="
input int TriangleRoundRadius         = 1;                              // Triangle Round Radius
input double TriangleBaseWidthPercent = 65.0;                           // Triangle Base Width Percent (of button size)
input double TriangleHeightPercent    = 70.0;                           // Triangle Height Percent (of base width)
input bool ShowUpDownButtons          = false;                          // Show Up/Down Buttons
input int TextFontSize = 17;                                            // Text Font Size

// Added a new global variable for supersampling factor
const int smoothness_factor = 10;  // Higher = smoother drag (e.g., 20 for more)
const int supersamplingFactor = 4;      // Supersampling for smooth rounds

Zunächst erweitern wir die Canvas-Objekte, indem wir zusätzliche hochauflösende Versionen für die Statistik- und Textpanels deklarieren, nämlich „canvasStatsHighRes“ und „canvasTextHighRes“, zusätzlich zu den bereits vorhandenen „canvasGraph“, „canvasStats“, „canvasHeader“ und „canvasText“, um das Supersampling-Rendering zu unterstützen. Anschließend definieren wir auch entsprechende Namen für diese neuen hochauflösenden Zeichenflächen, indem wir „canvasStatsHighResName“ mit dem Wert „StatsCanvasHighRes“ und „canvasTextHighResName“ mit dem Wert „TextCanvasHighRes“ festlegen, die im Programm zur Identifizierung und Erstellung verwendet werden. Wir haben die spezifischen Änderungen zur Verdeutlichung hervorgehoben.

Unter einer neuen Eingabegruppe mit der Bezeichnung „=== TEXT PANEL SETTINGS ===“ führen wir vom Benutzer konfigurierbare Parameter ein, darunter „TriangleRoundRadius“ zur Steuerung der Eckenrundung der Pfeile, „TriangleBaseWidthPercent“ und „TriangleHeightPercent“ zur Anpassung der Pfeilproportionen im Verhältnis zur Schaltflächengröße, „ShowUpDownButtons“ zum Ein- und Ausblenden der Schaltflächen der Bildlaufleiste sowie „TextFontSize“ zur Festlegung der Textanzeigegröße. Wir möchten insbesondere die Bildlaufleiste an die neue Version von Windows 11 anpassen, die zum Stand von 2026 die aktuellste Version ist. Abschließend definieren wir zwei Konstanten: „smoothness_factor“, der auf 10 initialisiert wird, um glattere Scroll-Interaktionen zu ermöglichen, und „supersamplingFactor“, der auf 4 gesetzt wird, um ein hochauflösendes Rendering zu ermöglichen, indem die Pixelabmessungen vor dem Herunterskalieren zur Kantenglättung multipliziert werden.

Stell dir das so vor: Man zeichnet alles viermal so groß und komprimiert es dann wieder, damit es superglatt wird. Dies ist entscheidend für das Anti-Aliasing. Indem wir mit vierfacher Auflösung (größer) zeichnen, können wir beim Verkleinern Pixel mitteln und so gezackte Kanten an Kurven, Rahmen und Text beseitigen. Es verbessert die Grafikqualität insgesamt, beansprucht jedoch mehr Rechenleistung. Daher sollte man diese Technik gezielt einsetzen. Nun werden wir in der Ereignisbehandlung von OnInit zusätzlich zu den normalen Canvases die folgenden hochauflösenden Canvases wie folgt erstellen.

if (EnableStatsPanel) {                                   //--- Check stats panel enabled
   int statsX = currentCanvasX + currentWidth + PanelGap; //--- Compute stats X
   if (!canvasStats.CreateBitmapLabel(0, 0, canvasStatsName, statsX, currentCanvasY + header_height + gap_y, currentWidth / 2, currentHeight, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats canvas
      Print("Failed to create Stats Canvas");             //--- Log creation failure
   }
   if (!canvasStatsHighRes.Create(canvasStatsHighResName, (currentWidth / 2) * supersamplingFactor, currentHeight * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create stats high-res
      Print("Failed to create Stats High-Res Canvas");    //--- Log creation failure
   }
   statsCreated = true;                                   //--- Set stats created flag
}

if (EnableTextPanel) {                                    //--- Check text panel enabled
   int textY = currentCanvasY + header_height + gap_y + currentHeight + PanelGap; //--- Compute text Y
   int text_width = inner_header_width;                   //--- Set text width
   int text_height = TextPanelHeight;                     //--- Set text height
   if (!canvasText.CreateBitmapLabel(0, 0, canvasTextName, currentCanvasX, textY, text_width, text_height, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text canvas
      Print("Failed to create Text Canvas");              //--- Log creation failure
   }
   if (!canvasTextHighRes.Create(canvasTextHighResName, text_width * supersamplingFactor, text_height * supersamplingFactor, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Create text high-res
      Print("Failed to create Text High-Res Canvas");     //--- Log creation failure
   }
   textCreated = true;                                    //--- Set text created flag
}

In der Ereignisbehandlung von OnInit prüfen wir anhand des Eingabewerts „EnableStatsPanel“, ob das Statistik-Panel aktiviert ist, und berechnen in diesem Fall die X-Position für die Statistik-Zeichenfläche, indem wir die aktuelle X-Koordinate der Zeichenfläche, die Breite und den Abstand zum Panel addieren. Anschließend erstellen wir die Standard-Statistik-Zeichenfläche mithilfe der Methode „CreateBitmapLabel“ des Objekts „canvasStats“, wobei wir die Chart-ID, das Unterfenster, den Namen, die Position, die Abmessungen und das Farbformat COLOR_FORMAT_ARGB_NORMALIZE angeben. Sollte die Erstellung fehlschlagen, wird eine Fehlermeldung protokolliert, genau wie bei der vorherigen Version. Als Nächstes erstellen wir die hochauflösende Statistik-Zeichenfläche mit der Methode Create des Objekts „canvasStatsHighRes“, wobei wir den High-Res-Namen verwenden, die Breite durch Multiplikation der halben aktuellen Breite mit „supersamplingFactor“ skalieren, die Höhe auf ähnliche Weise skalieren und dasselbe Farbformat verwenden. Auch hier wird ein Fehlerprotokoll erstellt, falls der Vorgang fehlschlägt, bevor das Flag „statsCreated“ auf „true“ gesetzt wird.

Ebenso berechnen wir, wenn das Textpanel über „EnableTextPanel“ aktiviert ist, die Y-Position für die Text-Zeichenfläche, indem wir die aktuelle Y-Koordinate der Zeichenfläche, die Höhe der Kopfzeile, die Abstände, die aktuelle Höhe und den Abstand des Textpanels addieren, und legen dann die Textbreite so fest, dass sie der inneren Breite der Kopfzeile entspricht, sowie die Höhe auf „TextPanelHeight“. Wir erstellen die Standard-Text-Zeichenfläche mit „CreateBitmapLabel“ für „canvasText“, geben dabei die erforderlichen Parameter an und protokollieren Fehler. Anschließend erstellen wir die hochauflösende Zeichenfläche, indem wir die Funktion „Create“ für „canvasTextHighRes“ mit skalierten Abmessungen basierend auf „supersamplingFactor“ aufrufen und etwaige Probleme protokollieren; anschließend setzen wir das Flag „textCreated“ auf „true“. Da wir neue Canvases erstellt haben, müssen wir – genau wie bei den normalen Canvases – deren Löschung berücksichtigen, wenn sie in den jeweiligen Bereichen nicht mehr benötigt werden. Bei der Deinitialisierung wenden wir beispielsweise die folgende Logik an.

//+------------------------------------------------------------------+
//| Deinitialize expert                                              |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {           // Deinitialize expert advisor
   canvasHeader.Destroy();                  //--- Destroy header canvas
   if (graphCreated) canvasGraph.Destroy(); //--- Destroy graph if created
   if (statsCreated) {
      canvasStats.Destroy();                //--- Destroy stats if created
      canvasStatsHighRes.Destroy();         //--- Destroy stats high-res
   }
   if (textCreated) {
      canvasText.Destroy();                 //--- Destroy text if created
      canvasTextHighRes.Destroy();          //--- Destroy text high-res
   }
   ChartRedraw();                           //--- Redraw chart
}

Wir löschen einfach die hochauflösenden Zeichenflächen, wenn wir sie nicht mehr benötigen. Wir verwenden dasselbe Format in den Funktionen „ToggleMinimize“ und „CloseDashboard“. Als Nächstes müssen wir die Logik zum Zeichnen der neuen, schick abgerundeten Rechtecke für die Statistik-Überschriften und der abgerundeten Dreiecke für die Schaltflächen der Bildlaufleiste definieren, da wir die statischen Elemente ersetzen und stattdessen unsere eigenen benutzerdefinierten Elemente zeichnen möchten. Wir verwenden die folgende Logik.

//+------------------------------------------------------------------+
//| Draw rectangle corner arc with exact boundaries                  |
//+------------------------------------------------------------------+
void DrawRectCornerArcPrecise(CCanvas &cvs, int centerX, int centerY, int radius, int thickness, uint borderARGB,
                              double startAngle, double endAngle) {
   int halfThickness = thickness / 2;
   double outerRadius = (double)radius + halfThickness;
   double innerRadius = (double)radius - halfThickness;
   if(innerRadius < 0) innerRadius = 0;

   int pixelRange = (int)(outerRadius + 2);

   for(int deltaY = -pixelRange; deltaY <= pixelRange; deltaY++) {
      for(int deltaX = -pixelRange; deltaX <= pixelRange; deltaX++) {
         double distance = MathSqrt(deltaX * deltaX + deltaY * deltaY);
         if(distance < innerRadius || distance > outerRadius) continue;

         double angle = MathArctan2((double)deltaY, (double)deltaX);
         
         if(IsAngleBetween(angle, startAngle, endAngle))
            cvs.PixelSet(centerX + deltaX, centerY + deltaY, borderARGB);
      }
   }
}

//+------------------------------------------------------------------+
//| Fill quadrilateral                                               |
//+------------------------------------------------------------------+
void FillQuadrilateral(CCanvas &cvs, double &verticesX[], double &verticesY[], uint fillColor) {
   double minY = verticesY[0], maxY = verticesY[0];
   for(int i = 1; i < 4; i++) {
      if(verticesY[i] < minY) minY = verticesY[i];
      if(verticesY[i] > maxY) maxY = verticesY[i];
   }

   int yStart = (int)MathCeil(minY);
   int yEnd = (int)MathFloor(maxY);

   for(int y = yStart; y <= yEnd; y++) {
      double scanlineY = (double)y + 0.5;
      double xIntersections[8];
      int intersectionCount = 0;

      for(int i = 0; i < 4; i++) {
         int nextIndex = (i + 1) % 4;
         double x0 = verticesX[i], y0 = verticesY[i];
         double x1 = verticesX[nextIndex], y1 = verticesY[nextIndex];

         double edgeMinY = (y0 < y1) ? y0 : y1;
         double edgeMaxY = (y0 > y1) ? y0 : y1;

         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         if(MathAbs(y1 - y0) < 1e-12) continue;

         double interpolationFactor = (scanlineY - y0) / (y1 - y0);
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         xIntersections[intersectionCount++] = x0 + interpolationFactor * (x1 - x0);
      }

      for(int a = 0; a < intersectionCount - 1; a++)
         for(int b = a + 1; b < intersectionCount; b++)
            if(xIntersections[a] > xIntersections[b]) {
               double temp = xIntersections[a];
               xIntersections[a] = xIntersections[b];
               xIntersections[b] = temp;
            }

      for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) {
         int xLeft = (int)MathCeil(xIntersections[pairIndex]);
         int xRight = (int)MathFloor(xIntersections[pairIndex + 1]);
         for(int x = xLeft; x <= xRight; x++)
            cvs.PixelSet(x, y, fillColor);
      }
   }
}

//+------------------------------------------------------------------+
//| Normalize angle                                                  |
//+------------------------------------------------------------------+
double NormalizeAngle(double angle) {
   double twoPi = 2.0 * M_PI;
   angle = MathMod(angle, twoPi);
   if(angle < 0) angle += twoPi;
   return angle;
}

//+------------------------------------------------------------------+
//| Is angle between                                                 |
//+------------------------------------------------------------------+
bool IsAngleBetween(double angle, double startAngle, double endAngle) {
   angle = NormalizeAngle(angle);
   startAngle = NormalizeAngle(startAngle);
   endAngle = NormalizeAngle(endAngle);
   
   double span = NormalizeAngle(endAngle - startAngle);
   double relativeAngle = NormalizeAngle(angle - startAngle);
   
   return relativeAngle <= span;
}

Hier erstellen wir die Funktion „DrawRectCornerArcPrecise“, um präzise Eckbögen für abgerundete Rechteckrahmen auf einer hochauflösenden Zeichenfläche darzustellen. Dabei berechnen wir die halbe Strichstärke, um den inneren und äußeren Radius zu definieren, durchlaufen anschließend einen Pixelbereich um den Mittelpunkt herum, um Abstände und Winkel zu überprüfen, und setzen Pixel nur dann, wenn sie innerhalb des durch Start- und Endwinkel definierten Bogensegments liegen. Wir implementieren die Funktion „FillQuadrilateral“, um beliebige Viereckformen mithilfe des Scanline-Algorithmus auszufüllen. Dabei werden zunächst die minimalen und maximalen Y-Grenzwerte anhand der Eckpunkte ermittelt. Anschließend werden für jede Scanlinie Y die Kantenkreuzungspunkte berechnet, sortiert und anschließend die horizontalen Abschnitte zwischen den Schnittpunktpaaren gefüllt, um eine lückenlose Abdeckung zu gewährleisten.

Wir definieren die Funktion „NormalizeAngle“, um Winkel im Bereich von 0 bis 2 * pi zu normieren, indem wir den Modulo verwenden und negative Werte anpassen. Diese Standardisierung gewährleistet einheitliche Winkelvergleiche bei kreisförmigen Geometrien wie Bögen. Wir fügen die Funktion „IsAngleBetween“ hinzu, um festzustellen, ob ein bestimmter Winkel zwischen dem Start- und dem Endwinkel liegt. Wir normieren alle Eingabewerte, berechnen die Spannweite und prüfen die relative Position, wobei wir sowohl Drehungen im Uhrzeigersinn als auch gegen den Uhrzeigersinn unterstützen, um eine flexible Bogenwiedergabe zu ermöglichen. Mit diesen Funktionen können wir nun, wie unten gezeigt, ein abgerundetes Rechteck zeichnen.

//+------------------------------------------------------------------+
//| Fill rounded rectangle                                           |
//+------------------------------------------------------------------+
void FillRoundedRectangle(CCanvas &cvs, int x, int y, int w, int h, int radius, uint argb_color) { // Render rounded fill
   if (radius <= 0) {                                                           //--- Check zero radius
      cvs.FillRectangle(x, y, x + w - 1, y + h - 1, argb_color);                //--- Fill rectangle
      return;                                                                   //--- Exit function
   }
   radius = MathMin(radius, MathMin(w / 2, h / 2));                             //--- Adjust radius

   cvs.Arc(x + radius, y + radius, radius, radius, DegreesToRadians(180), DegreesToRadians(90), argb_color); //--- Draw top-left arc
   cvs.Arc(x + w - radius - 1, y + radius, radius, radius, DegreesToRadians(270), DegreesToRadians(90), argb_color); //--- Draw top-right arc
   cvs.Arc(x + w - radius - 1, y + h - radius - 1, radius, radius, DegreesToRadians(0), DegreesToRadians(90), argb_color); //--- Draw bottom-right arc
   cvs.Arc(x + radius, y + h - radius - 1, radius, radius, DegreesToRadians(90), DegreesToRadians(90), argb_color); //--- Draw bottom-left arc

   cvs.FillCircle(x + radius, y + radius, radius, argb_color);                  //--- Fill top-left circle
   cvs.FillCircle(x + w - radius - 1, y + radius, radius, argb_color);          //--- Fill top-right circle
   cvs.FillCircle(x + w - radius - 1, y + h - radius - 1, radius, argb_color);  //--- Fill bottom-right circle
   cvs.FillCircle(x + radius, y + h - radius - 1, radius, argb_color); //--- Fill bottom-left circle

   cvs.FillRectangle(x + radius, y, x + w - radius - 1, y + h - 1, argb_color); //--- Fill horizontal body

   cvs.FillRectangle(x, y + radius, x + w - 1, y + h - radius - 1, argb_color); //--- Fill vertical body
}

//+------------------------------------------------------------------+
//| Draw rounded rectangle border                                    |
//+------------------------------------------------------------------+
void DrawRoundedRectangleBorderHiRes(CCanvas &cvs, int positionX, int positionY, int width, int height, int radius, uint borderColorARGB, int thickness) {
   int scaledThickness = thickness;

   DrawRectStraightEdge(cvs, positionX + radius, positionY, positionX + width - radius, positionY, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX + width - radius, positionY + height - 1, positionX + radius, positionY + height - 1, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX, positionY + height - radius, positionX, positionY + radius, scaledThickness, borderColorARGB);
   DrawRectStraightEdge(cvs, positionX + width - 1, positionY + radius, positionX + width - 1, positionY + height - radius, scaledThickness, borderColorARGB);

   DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + radius, radius, scaledThickness, borderColorARGB, 
                            M_PI, M_PI * 1.5);
   DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 1.5, M_PI * 2.0);
   DrawRectCornerArcPrecise(cvs, positionX + radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            M_PI * 0.5, M_PI);
   DrawRectCornerArcPrecise(cvs, positionX + width - radius, positionY + height - radius, radius, scaledThickness, borderColorARGB,
                            0.0, M_PI * 0.5);
}

//+------------------------------------------------------------------+
//| Draw straight edge for rectangle border                          |
//+------------------------------------------------------------------+
void DrawRectStraightEdge(CCanvas &cvs, double startX, double startY, double endX, double endY, int thickness, uint borderARGB) {
   double deltaX = endX - startX;
   double deltaY = endY - startY;
   double edgeLength = MathSqrt(deltaX*deltaX + deltaY*deltaY);
   if(edgeLength < 1e-6) return;

   double perpendicularX = -deltaY / edgeLength;
   double perpendicularY = deltaX / edgeLength;

   double edgeDirectionX = deltaX / edgeLength;
   double edgeDirectionY = deltaY / edgeLength;

   double halfThickness = (double)thickness / 2.0;
   
   double extensionLength = 1.5;
   double extendedStartX = startX - edgeDirectionX * extensionLength;
   double extendedStartY = startY - edgeDirectionY * extensionLength;
   double extendedEndX = endX + edgeDirectionX * extensionLength;
   double extendedEndY = endY + edgeDirectionY * extensionLength;

   double verticesX[4], verticesY[4];
   verticesX[0] = extendedStartX - perpendicularX * halfThickness;  verticesY[0] = extendedStartY - perpendicularY * halfThickness;
   verticesX[1] = extendedStartX + perpendicularX * halfThickness;  verticesY[1] = extendedStartY + perpendicularY * halfThickness;
   verticesX[2] = extendedEndX + perpendicularX * halfThickness;  verticesY[2] = extendedEndY + perpendicularY * halfThickness;
   verticesX[3] = extendedEndX - perpendicularX * halfThickness;  verticesY[3] = extendedEndY - perpendicularY * halfThickness;

   FillQuadrilateral(cvs, verticesX, verticesY, borderARGB);
}

Um ein abgerundetes Rechteck zu erstellen, implementieren wir zunächst die Funktion „FillRoundedRectangle“, um gefüllte, abgerundete Rechtecke auf der Zeichenfläche darzustellen. Dabei behandeln wir zunächst Fälle mit einem Radius von null, indem wir ein Standardrechteck füllen, passen dann den Radius so an, dass er innerhalb der Hälfte der Breite oder Höhe liegt, zeichnen an jeder Ecke Viertelbögen mithilfe der Arc-Methode, wobei wir Gradwerte in Radiant umrechnen, füllen die Ecken mit vollständigen Kreisen, um eine lückenlose Abdeckung zu gewährleisten, und füllen schließlich die horizontalen und vertikalen Körperabschnitte aus, um die abgerundeten Teile nahtlos miteinander zu verbinden.

Anschließend erstellen wir die Funktion „DrawRoundedRectangleBorderHiRes“ zum Zeichnen hochauflösender Rahmen um abgerundete Rechtecke, wobei wir die Dicke skalieren, „DrawRectStraightEdge“ zum Rendern der vier geraden Seiten aufrufen und „DrawRectCornerArcPrecise“ für jeden Eckbogen mit spezifischen Start- und Endwinkeln in Radianten verwenden (z. B. von M_PI bis M_PI * 1,5 für die obere linke Ecke), um präzise, kantenglättete Umrisse ohne Überlappungen zu gewährleisten.

Schließlich berechnen wir in der Funktion „DrawRectStraightEdge“ die Deltas und normalisieren die Vektoren für die Kantenrichtung und die Senkrechte, ermitteln die halbe Dicke für die Randbreite, verlängern die Linie leicht über die Endpunkte hinaus, um glatte Eckverbindungen zu gewährleisten, definieren vier Eckpunkte für einen viereckigen Streifen entlang der Kante und übergeben diese an „FillQuadrilateral“, um das Randsegment auszufüllen. So entsteht durch vektorbasierte Geometrie das Erscheinungsbild einer durchgehenden, hochwertigen geraden Linie. Dies sind die Funktionen, die wir aufrufen werden, um das gewünschte abgerundete Rechteck zu erstellen. Wir können nun eine Logik für ein abgerundetes Dreieck definieren. Zunächst benötigen wir Hilfsfunktionen.

//+------------------------------------------------------------------+
//| Precompute triangle geometry                                     |
//+------------------------------------------------------------------+
void PrecomputeTriangleGeometry(double &sharpX[], double &sharpY[], int radius, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) {
   for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {
      int previousIndex = (cornerIndex + 2) % 3;
      int nextIndex = (cornerIndex + 1) % 3;

      double edgeA_X = sharpX[cornerIndex] - sharpX[previousIndex], edgeA_Y = sharpY[cornerIndex] - sharpY[previousIndex];
      double edgeA_Length = MathSqrt(edgeA_X*edgeA_X + edgeA_Y*edgeA_Y);
      edgeA_X /= edgeA_Length; edgeA_Y /= edgeA_Length;

      double edgeB_X = sharpX[nextIndex] - sharpX[cornerIndex], edgeB_Y = sharpY[nextIndex] - sharpY[cornerIndex];
      double edgeB_Length = MathSqrt(edgeB_X*edgeB_X + edgeB_Y*edgeB_Y);
      edgeB_X /= edgeB_Length; edgeB_Y /= edgeB_Length;

      double normalA_X = edgeA_Y, normalA_Y = -edgeA_X;
      double normalB_X = edgeB_Y, normalB_Y = -edgeB_X;

      double bisectorX = normalA_X + normalB_X, bisectorY = normalA_Y + normalB_Y;
      double bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY);
      if(bisectorLength < 1e-12) { bisectorX = normalA_X; bisectorY = normalA_Y; bisectorLength = MathSqrt(bisectorX*bisectorX + bisectorY*bisectorY); }
      bisectorX /= bisectorLength; bisectorY /= bisectorLength;

      double cosInteriorAngle = (-edgeA_X)*edgeB_X + (-edgeA_Y)*edgeB_Y;
      if(cosInteriorAngle > 1.0) cosInteriorAngle = 1.0;
      if(cosInteriorAngle < -1.0) cosInteriorAngle = -1.0;
      double halfAngle = MathArccos(cosInteriorAngle) / 2.0;
      double sinHalfAngle = MathSin(halfAngle);
      if(sinHalfAngle < 1e-12) sinHalfAngle = 1e-12;

      double distanceToCenter = radius / sinHalfAngle;
      arcCentersX[cornerIndex] = sharpX[cornerIndex] + bisectorX * distanceToCenter;
      arcCentersY[cornerIndex] = sharpY[cornerIndex] + bisectorY * distanceToCenter;

      double deltaX_A = sharpX[cornerIndex] - sharpX[previousIndex], deltaY_A = sharpY[cornerIndex] - sharpY[previousIndex];
      double lengthSquared_A = deltaX_A*deltaX_A + deltaY_A*deltaY_A;
      double interpolationFactor_A = ((arcCentersX[cornerIndex] - sharpX[previousIndex])*deltaX_A + (arcCentersY[cornerIndex] - sharpY[previousIndex])*deltaY_A) / lengthSquared_A;
      tangentX[cornerIndex][1] = sharpX[previousIndex] + interpolationFactor_A * deltaX_A;
      tangentY[cornerIndex][1] = sharpY[previousIndex] + interpolationFactor_A * deltaY_A;

      double deltaX_B = sharpX[nextIndex] - sharpX[cornerIndex], deltaY_B = sharpY[nextIndex] - sharpY[cornerIndex];
      double lengthSquared_B = deltaX_B*deltaX_B + deltaY_B*deltaY_B;
      double interpolationFactor_B = ((arcCentersX[cornerIndex] - sharpX[cornerIndex])*deltaX_B + (arcCentersY[cornerIndex] - sharpY[cornerIndex])*deltaY_B) / lengthSquared_B;
      tangentX[cornerIndex][0] = sharpX[cornerIndex] + interpolationFactor_B * deltaX_B;
      tangentY[cornerIndex][0] = sharpY[cornerIndex] + interpolationFactor_B * deltaY_B;

      startAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][1] - arcCentersY[cornerIndex], tangentX[cornerIndex][1] - arcCentersX[cornerIndex]);
      endAngles[cornerIndex] = MathArctan2(tangentY[cornerIndex][0] - arcCentersY[cornerIndex], tangentX[cornerIndex][0] - arcCentersX[cornerIndex]);
   }
}

//+------------------------------------------------------------------+
//| Angle in arc sweep for triangle                                  |
//+------------------------------------------------------------------+
bool TriangleAngleInArcSweep(double startAngle, double endAngle, double angle) {
   double twoPi = 2.0 * M_PI;
   double startAngleMod = MathMod(startAngle + twoPi, twoPi);
   double endAngleMod = MathMod(endAngle + twoPi, twoPi);
   angle = MathMod(angle + twoPi, twoPi);

   double ccwSpan = MathMod(endAngleMod - startAngleMod + twoPi, twoPi);

   if(ccwSpan <= M_PI) {
      double relativeAngle = MathMod(angle - startAngleMod + twoPi, twoPi);
      return(relativeAngle <= ccwSpan + 1e-6);
   } else {
      double cwSpan = twoPi - ccwSpan;
      double relativeAngle = MathMod(angle - endAngleMod + twoPi, twoPi);
      return(relativeAngle <= cwSpan + 1e-6);
   }
}

Wir definieren die Funktion „PrecomputeTriangleGeometry“, um die für die Darstellung abgerundeter Dreiecke erforderlichen geometrischen Elemente zu berechnen – wie beispielsweise jene, die wir in unseren benutzerdefinierten Pfeilsymbolen verwenden werden –, indem wir mithilfe einer Schleife mit dem „cornerIndex“ von 0 bis 2 nacheinander alle drei Ecken durchlaufen. Für jede Ecke bestimmen wir den vorherigen und den nächsten Index modulo 3 für den zyklischen Zugriff, berechnen normalisierte Kantenvektoren von den scharfen Eckpunkten „sharpX“ und „sharpY“ zu benachbarten Punkten und leiten die nach außen gerichteten Normalen ab, indem wir diese Vektoren um 90 Grad drehen. Anschließend bilden wir die Winkelhalbierende, indem wir die Normalen addieren und sie nach der Behandlung von Fällen mit Längen nahe Null normalisieren, und berechnen den Kosinus des Innenwinkels über das Skalarprodukt der negierten Kantenvektoren, wobei wir den Wert auf den Bereich zwischen -1 und 1 beschränken, bevor wir den Halbwinkel und dessen Sinus ermitteln, um eine Division durch Null zu vermeiden.

Anhand des Radius, geteilt durch diesen Sinus, positionieren wir den Bogenmittelpunkt entlang der Winkelhalbierenden ausgehend von der Ecke, um eine gleichmäßige Rundung zu gewährleisten; anschließend projizieren wir den Bogenmittelpunkt auf die Kanten, um die in „tangentX“ und „tangentY“ gespeicherten Tangentialpunkte zu ermitteln, und berechnen mit MathArctan2 die Start- und Endwinkel für den Bogenverlauf an dieser Ecke.

Diese Vorberechnung ist entscheidend für die präzise, kantengeglättete Darstellung abgerundeter Formen, da sie scharfe Dreiecksscheitelpunkte in glatte Kurven umwandelt, indem sie Bogenmittelpunkte „arcCentersX“ und „arcCentersY“, Tangenten sowie Winkel „startAngles“ und „endAngles“ definiert. Dies ermöglicht eine vektorbasierte Füllung ohne Pixelartefakte und verbessert die visuelle Qualität von UI-Elementen wie Scroll-Pfeilen erheblich, da während der Pixeliteration genaue Randprüfungen durchgeführt werden können.

Anschließend implementieren wir die Funktion „TriangleAngleInArcSweep“, um zu überprüfen, ob ein gegebener Winkel innerhalb eines durch Start- und Endwinkel definierten Bogens liegt. Dazu normalisieren wir zunächst alle Winkel mit MathMod in den Bereich von 0 bis 2 * pi, wobei wir 2 * pi addieren, um negative Werte in den positiven Bereich zu überführen. Wir berechnen die Spannweite gegen den Uhrzeigersinn und prüfen, ob sie pi oder kleiner ist; in diesem Fall überprüfen wir den relativen Winkel vom Startpunkt aus. Andernfalls verwenden wir die Spannweite im Uhrzeigersinn und prüfen vom Endpunkt aus, wobei wir ein kleines Epsilon für die Gleitkommagenauigkeit hinzufügen. Dies unterstützt beide Bogenrichtungen und gewährleistet die korrekte Einbeziehung beim Ausfüllen von Scanlinien oder beim Pixeltest beim Rendern abgerundeter Dreiecke. Falls Sie sich fragen, was es mit diesem Scanline-Algorithmus eigentlich auf sich hat, möchten wir Ihnen das kurz erläutern, 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 Figur 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 Begrenzung des Polygons stößt, hält man entsprechend an oder setzt das Zeichnen fort. Der Algorithmus folgt demselben Prinzip. In der folgenden Abbildung wird dieses Verhalten veranschaulicht: Die roten Punkte stellen die Eckpunkte des Polygons dar, während die blauen Punkte die Schnittpunkte entlang der Scanlinie kennzeichnen.

SCANLINE-ALGORITHMUS

In der Hoffnung, dass dies verständlich ist, können wir nun damit fortfahren, die Funktionen zu definieren, die uns beim Zeichnen der abgerundeten Dreiecke helfen, die wir für die Pfeile verwenden werden.

//+------------------------------------------------------------------+
//| Fill rounded triangle                                            |
//+------------------------------------------------------------------+
void FillRoundedTriangle(CCanvas &cvs, double &sharpX[], double &sharpY[], int radius, uint fillColor, double &arcCentersX[], double &arcCentersY[], double &tangentX[][2], double &tangentY[][2], double &startAngles[], double &endAngles[]) {
   double minY = sharpY[0], maxY = sharpY[0];
   for(int i = 1; i < 3; i++) {
      if(sharpY[i] < minY) minY = sharpY[i];
      if(sharpY[i] > maxY) maxY = sharpY[i];
   }

   int yStart = (int)MathCeil(minY);
   int yEnd = (int)MathFloor(maxY);

   for(int y = yStart; y <= yEnd; y++) {
      double scanlineY = (double)y + 0.5;

      double xIntersections[12];
      int intersectionCount = 0;

      for(int edgeIndex = 0; edgeIndex < 3; edgeIndex++) {
         int nextIndex = (edgeIndex + 1) % 3;
         double startX = tangentX[edgeIndex][0], startY = tangentY[edgeIndex][0];
         double endX = tangentX[nextIndex][1], endY = tangentY[nextIndex][1];

         double edgeMinY = (startY < endY) ? startY : endY;
         double edgeMaxY = (startY > endY) ? startY : endY;

         if(scanlineY < edgeMinY || scanlineY > edgeMaxY) continue;
         if(MathAbs(endY - startY) < 1e-12) continue;

         double interpolationFactor = (scanlineY - startY) / (endY - startY);
         if(interpolationFactor < 0.0 || interpolationFactor > 1.0) continue;

         xIntersections[intersectionCount++] = startX + interpolationFactor * (endX - startX);
      }

      for(int cornerIndex = 0; cornerIndex < 3; cornerIndex++) {
         double centerX = arcCentersX[cornerIndex], centerY = arcCentersY[cornerIndex];
         double deltaY = scanlineY - centerY;

         if(MathAbs(deltaY) > radius) continue;

         double deltaX = MathSqrt(radius*radius - deltaY*deltaY);

         double candidates[2];
         candidates[0] = centerX - deltaX;
         candidates[1] = centerX + deltaX;

         for(int candidateIndex = 0; candidateIndex < 2; candidateIndex++) {
            double angle = MathArctan2(scanlineY - centerY, candidates[candidateIndex] - centerX);
            if(TriangleAngleInArcSweep(startAngles[cornerIndex], endAngles[cornerIndex], angle))
               xIntersections[intersectionCount++] = candidates[candidateIndex];
         }
      }

      for(int a = 0; a < intersectionCount - 1; a++)
         for(int b = a + 1; b < intersectionCount; b++)
            if(xIntersections[a] > xIntersections[b]) {
               double temp = xIntersections[a];
               xIntersections[a] = xIntersections[b];
               xIntersections[b] = temp;
            }

      for(int pairIndex = 0; pairIndex + 1 < intersectionCount; pairIndex += 2) {
         int xLeft = (int)MathCeil(xIntersections[pairIndex]);
         int xRight = (int)MathFloor(xIntersections[pairIndex + 1]);
         for(int x = xLeft; x <= xRight; x++)
            cvs.PixelSet(x, y, fillColor);
      }
   }
}

//+------------------------------------------------------------------+
//| Draw rounded triangle arrow                                      |
//+------------------------------------------------------------------+
void DrawRoundedTriangleArrow(CCanvas &cvs, int baseX, int baseY, int tri_base_width, int tri_height, bool isUp, uint fillColor) {
   int radius = TriangleRoundRadius * supersamplingFactor;

   double sharpX[3], sharpY[3];

   if (isUp) {
      sharpX[0] = baseX;
      sharpY[0] = baseY;
      sharpX[1] = baseX - tri_base_width / 2.0;
      sharpY[1] = baseY + tri_height;
      sharpX[2] = baseX + tri_base_width / 2.0;
      sharpY[2] = baseY + tri_height;
   } else {
      sharpX[0] = baseX;
      sharpY[0] = baseY + tri_height;
      sharpX[1] = baseX + tri_base_width / 2.0;  // Swapped for consistent winding
      sharpY[1] = baseY;
      sharpX[2] = baseX - tri_base_width / 2.0;
      sharpY[2] = baseY;
   }

   double arcCentersX[3], arcCentersY[3];
   double tangentX[3][2], tangentY[3][2];
   double startAngles[3], endAngles[3];

   PrecomputeTriangleGeometry(sharpX, sharpY, radius, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles);

   FillRoundedTriangle(cvs, sharpX, sharpY, radius, fillColor, arcCentersX, arcCentersY, tangentX, tangentY, startAngles, endAngles);
}

Wir implementieren die Funktion „FillRoundedTriangle“, um gefüllte, abgerundete Dreiecke mithilfe eines Ansatzes auf der Basis des Scanline-Algorithmus mit präziser Füllung in Vektorqualität. Dabei ermitteln wir zunächst die minimalen und maximalen Y-Koordinaten der scharfen Eckpunkte „sharpY“, um die vertikalen Grenzen zu definieren, und durchlaufen dann alle ganzzahligen y-Werte von der Obergrenze von minY bis zur Untergrenze von maxY mit einem Halbpixel-Versatz „scanlineY“, um durch Subpixel-Genauigkeit ein besseres Anti-Aliasing zu erzielen. Für jede Scanlinie berechnen wir bis zu 12 x-Schnittpunkte: zuerst mit den drei geraden Tangentenkanten durch lineare Interpolation zwischen den Tangentenpunkten "tangentX" und "tangentY", falls die Scanlinie innerhalb des Y-Bereichs der Kante liegt, und dann für jede der drei Ecken, indem wir prüfen, ob die Scanlinie den Bogen schneidet, indem wir deltaX aus der Kreisgleichung im Bogenmittelpunkt "arcCentersX" und "arcCentersY" mit dem gegebenen Radius berechnen. Kandidaten-x-Werte werden nur dann hinzugefügt, wenn ihre Winkel die "TriangleAngleInArcSweep"-Prüfung bestehen, um sicherzustellen, dass sie innerhalb des durch "startAngles" und "endAngles" definierten Bogenbereichs liegen.

Wir sortieren die gesammelten x-Schnittpunkte in aufsteigender Reihenfolge mithilfe eines einfachen Bubble-Sorts, um bei kleinen Arrays die Effizienz zu gewährleisten. Anschließend füllen wir die horizontalen Bereiche zwischen jedem Schnittpunktpaar, indem wir die Pixel vom aufgerundeten linken x-Wert bis zum abgerundeten rechten x-Wert mit der angegebenen „fillColor“ ausfüllen. Das Ergebnis ist ein glattes, artefaktfreies, abgerundetes Dreieck, das vorberechnete Geometriedaten nutzt, um eine hochwertige Darstellung in UI-Elementen wie Pfeilen zu gewährleisten. Diese Methode des Scanline-Algorithmus ist von großer Bedeutung, da sie eine pixelgenaue Füllung komplexer, gekrümmter Formen ermöglicht, ohne auf Näherungsverfahren zurückgreifen zu müssen. Durch die präzise Bestimmung von Schnittpunkten an den Rändern werden glatte Kanten gewährleistet – was entscheidend für die Aufrechterhaltung der visuellen Klarheit in skalierten oder hochauflösenden Kontexten ist, in denen herkömmliche Polygonfüllungen zu Treppchen oder Lücken führen können.

Wir erstellen die Funktion „DrawRoundedTriangleArrow“, um nach oben oder unten abgerundete Dreieckspfeile für unsere Schaltflächen der Bildlaufleiste zu zeichnen. Dabei skalieren wir den „TriangleRoundRadius“ mit dem „supersamplingFactor“, um ein hochauflösendes Rendering zu gewährleisten, und definieren die drei scharfen Eckpunkte „sharpX“ und „sharpY“ basierend auf der Basisposition, der Dreiecksbasisbreite „tri_base_width“, der Höhe „tri_height“ und der Richtung „isUp“ – bei Aufwärtspfeilen wird die Spitze oben und die Basis unten platziert, während bei Abwärtspfeilen die Basis-Punkte vertauscht werden, um eine konsistente Windungsreihenfolge zu gewährleisten und Füllartefakte zu vermeiden. Anschließend rufen wir „PrecomputeTriangleGeometry“ auf, um aus diesen Eckpunkten und dem Radius die Bogenmittelpunkte, Tangenten und Winkel zu berechnen, und speichern diese in den Arrays „arcCentersX“, „arcCentersY“, „tangentX“, „tangentY“, „startAngles“ und „endAngles“. Anschließend rufen wir „FillRoundedTriangle“ mit diesen Parametern und der „fillColor“ auf, um das eigentliche Rendering durchzuführen. So entstehen glatte, abgerundete Pfeile, die sich nahtlos in die Bildlaufleiste einfügen und so die Ästhetik und Benutzerfreundlichkeit der Benutzeroberfläche verbessern. Die Hochskalierung ist nun abgeschlossen; nun ist es an der Zeit, für die endgültige Darstellung auf der Zeichenfläche ein Herunterskalieren vorzunehmen.

//+------------------------------------------------------------------+
//| Downsample canvas (average for AA)                               |
//+------------------------------------------------------------------+
void DownsampleCanvas(CCanvas &targetCanvas, CCanvas &highResCanvas) {
   int targetWidth  = targetCanvas.Width();
   int targetHeight = targetCanvas.Height();

   for(int pixelY = 0; pixelY < targetHeight; pixelY++) {
      for(int pixelX = 0; pixelX < targetWidth; pixelX++) {
         double sourceX = pixelX * supersamplingFactor;
         double sourceY = pixelY * supersamplingFactor;

         double sumAlpha = 0, sumRed = 0, sumGreen = 0, sumBlue = 0;
         double weightSum = 0;

         for(int deltaY = 0; deltaY < supersamplingFactor; deltaY++) {
            for(int deltaX = 0; deltaX < supersamplingFactor; deltaX++) {
               int sourcePixelX = (int)(sourceX + deltaX);
               int sourcePixelY = (int)(sourceY + deltaY);

               if(sourcePixelX >= 0 && sourcePixelX < highResCanvas.Width() && sourcePixelY >= 0 && sourcePixelY < highResCanvas.Height()) {
                  uint pixelValue = highResCanvas.PixelGet(sourcePixelX, sourcePixelY);

                  uchar alpha = (uchar)((pixelValue >> 24) & 0xFF);
                  uchar red = (uchar)((pixelValue >> 16) & 0xFF);
                  uchar green = (uchar)((pixelValue >> 8)  & 0xFF);
                  uchar blue = (uchar)(pixelValue         & 0xFF);

                  double weight = 1.0;
                  sumAlpha += alpha * weight;
                  sumRed += red * weight;
                  sumGreen += green * weight;
                  sumBlue += blue * weight;
                  weightSum += weight;
               }
            }
         }

         if(weightSum > 0) {
            uchar finalAlpha = (uchar)(sumAlpha / weightSum);
            uchar finalRed = (uchar)(sumRed / weightSum);
            uchar finalGreen = (uchar)(sumGreen / weightSum);
            uchar finalBlue = (uchar)(sumBlue / weightSum);

            uint finalColor = ((uint)finalAlpha << 24) | ((uint)finalRed << 16) |
                              ((uint)finalGreen << 8)  | (uint)finalBlue;
            targetCanvas.PixelSet(pixelX, pixelY, finalColor);
         }
      }
   }
}

Wir definieren die Funktion „DownsampleCanvas“, um ein Anti-Aliasing durchzuführen, indem wir eine hochauflösende Zeichenfläche durch Pixelmittelung auf die Zielgröße verkleinern. Dabei entnehmen wir die Zielabmessungen aus „targetCanvas.Width()“ und „targetCanvas.Height()“ und durchlaufen anschließend jedes Zielpixel mit verschachtelten Schleifen für „pixelY“ und „pixelX“. Jedes Zielpixel ordnen wir dem Quellbereich zu, indem wir die Koordinaten mit dem „supersamplingFactor“ multiplizieren, um „sourceX“ und „sourceY“ zu erhalten. Dabei initialisieren wir die Summen für Alpha, Rot, Grün, Blau sowie einen Zähler „weightSum“, bevor wir verschachtelte Schleifen über „deltaY“ und „deltaX“ von 0 bis „supersamplingFactor“ - 1 durchlaufen und die entsprechenden hochauflösenden Pixel abtasten.

Wir berechnen die Positionen der Quellpixel, prüfen, ob sie innerhalb der Grenzen von „highResCanvas.Width()“ und „highResCanvas.Height()“ ab, extrahieren die ARGB-Komponenten mithilfe von PixelGet und Bitverschiebungen, addieren gewichtete Beiträge (mit einheitlichem Gewicht 1,0) zu den Summen und, falls weightSum positiv ist, mitteln wir jede Komponente, um die endgültigen uchar-Werte zu berechnen, kombinieren diese mithilfe von Bitverschiebungen zu finalColor und setzen diesen Wert über die Methode PixelSet auf das Ziel. Dieser Herunterskalierungsprozess ist unerlässlich, um glatte, Anti-Aliasing-behandelte Darstellungen zu erzielen, indem mehrere hochauflösende Samples pro Zielpixel miteinander vermischt werden. Dadurch werden gezackte Kanten bei gerenderten Elementen wie Rahmen und Formen reduziert, was die grafische Gesamtqualität des Dashboards ohne hohen Rechenaufwand verbessert. Wir haben nun alle Hilfsfunktionen, die wir benötigen, also können wir loslegen. Zunächst werden wir die Statistik-Zeichenfläche aktualisieren. Dabei zeichnen wir zunächst auf einer großen, hochauflösenden Zeichenfläche und verkleinern das Bild anschließend auf die normale Größe.

//+------------------------------------------------------------------+
//| Update stats on canvas                                           |
//+------------------------------------------------------------------+
void UpdateStatsOnCanvas() {            // Render stats elements
   canvasStatsHighRes.Erase(0);         //--- Clear high-res stats canvas

   int statsWidthHighRes = (currentWidth / 2) * supersamplingFactor; //--- Scaled width
   int heightHighRes = currentHeight * supersamplingFactor;          //--- Scaled height

   if (UseBackground && ArraySize(bg_pixels_stats) == (currentWidth / 2) * currentHeight) { //--- Check background valid
      uint bg_pixels_stats_high[];                                   //--- High-res bg
      ArrayResize(bg_pixels_stats_high, statsWidthHighRes * heightHighRes); //--- Resize
      ScaleImage(bg_pixels_stats_high, currentWidth / 2, currentHeight, statsWidthHighRes, heightHighRes); //--- Scale bg
      for (int y = 0; y < heightHighRes; y++) {                      //--- Loop Y
         for (int x = 0; x < statsWidthHighRes; x++) {               //--- Loop X
            canvasStatsHighRes.PixelSet(x, y, bg_pixels_stats_high[y * statsWidthHighRes + x]); //--- Set pixel
         }
      }
   }

   if (StatsBackgroundMode != NoColor) {                             //--- Check background mode
      for (int y = 0; y < heightHighRes; y++) {                      //--- Loop rows
         double factor = (double)y / (heightHighRes - 1);            //--- Compute factor
         color currentColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get color
         uchar alpha = (uchar)(255 * BackgroundOpacity);             //--- Compute alpha
         uint argbFill = ColorToARGB(currentColor, alpha);           //--- Convert to ARGB

         for (int x = 0; x < statsWidthHighRes; x++) {               //--- Loop columns
            uint currentPixel = canvasStatsHighRes.PixelGet(x, y);   //--- Get pixel
            uint blendedPixel = BlendPixels(currentPixel, argbFill); //--- Blend pixels
            canvasStatsHighRes.PixelSet(x, y, blendedPixel);         //--- Set blended pixel
         }
      }
   }

   if (StatsBackgroundMode != NoColor) {                              //--- Check background mode for borders
      double reduction = BorderOpacityPercentReduction / 100.0;       //--- Compute reduction
      double opacity = MathMax(0.0, MathMin(1.0, BackgroundOpacity * (1.0 - reduction))); //--- Compute opacity
      uchar alpha = (uchar)(255 * opacity);                           //--- Set alpha
      double darkenReduction = BorderDarkenPercent / 100.0;           //--- Compute darken reduction
      double darkenFactor = MathMax(0.0, MathMin(1.0, 1.0 - darkenReduction)); //--- Compute darken factor

      for (int y = 0; y < heightHighRes; y++) {                       //--- Loop vertical borders
         double factor = (StatsBackgroundMode == SingleColor) ? 0.0 : (double)y / (heightHighRes - 1); //--- Get factor
         color baseColor = (StatsBackgroundMode == SingleColor) ? GetTopColor() : InterpolateColor(GetTopColor(), GetBottomColor(), factor); //--- Get base color
         color darkColor = DarkenColor(baseColor, darkenFactor);      //--- Darken color
         uint argb = ColorToARGB(darkColor, alpha);                   //--- Convert to ARGB

         canvasStatsHighRes.PixelSet(0, y, argb);                     //--- Set left border pixel
         canvasStatsHighRes.PixelSet(1, y, argb);                     //--- Set inner left pixel

         canvasStatsHighRes.PixelSet(statsWidthHighRes - 1, y, argb); //--- Set right border pixel
         canvasStatsHighRes.PixelSet(statsWidthHighRes - 2, y, argb); //--- Set inner right pixel
      }

      double factorTop = 0.0;                                         //--- Set top factor
      color baseTop = GetTopColor();                                  //--- Get top base
      color darkTop = DarkenColor(baseTop, darkenFactor);             //--- Darken top
      uint argbTop = ColorToARGB(darkTop, alpha);                     //--- Convert top to ARGB
      for (int x = 0; x < statsWidthHighRes; x++) {                   //--- Loop top borders
         canvasStatsHighRes.PixelSet(x, 0, argbTop);                  //--- Set top pixel
         canvasStatsHighRes.PixelSet(x, 1, argbTop);                  //--- Set inner top pixel
      }

      double factorBot = (StatsBackgroundMode == SingleColor) ? 0.0 : 1.0; //--- Set bottom factor
      color baseBot = (StatsBackgroundMode == SingleColor) ? GetTopColor() : GetBottomColor(); //--- Get bottom base
      color darkBot = DarkenColor(baseBot, darkenFactor);                  //--- Darken bottom
      uint argbBot = ColorToARGB(darkBot, alpha);                          //--- Convert bottom to ARGB
      for (int x = 0; x < statsWidthHighRes; x++) {                        //--- Loop bottom borders
         canvasStatsHighRes.PixelSet(x, heightHighRes - 1, argbBot);       //--- Set bottom pixel
         canvasStatsHighRes.PixelSet(x, heightHighRes - 2, argbBot);       //--- Set inner bottom pixel
      }
   } else {                                                                //--- Handle no background
      uint argbBorder = ColorToARGB(GetBorderColor(), 255);                //--- Convert border to ARGB
      canvasStatsHighRes.Line(0, 0, statsWidthHighRes - 1, 0, argbBorder); //--- Draw top border
      canvasStatsHighRes.Line(statsWidthHighRes - 1, 0, statsWidthHighRes - 1, heightHighRes - 1, argbBorder); //--- Draw right border
      canvasStatsHighRes.Line(statsWidthHighRes - 1, heightHighRes - 1, 0, heightHighRes - 1, argbBorder); //--- Draw bottom border
      canvasStatsHighRes.Line(0, heightHighRes - 1, 0, 0, argbBorder);     //--- Draw left border
      canvasStatsHighRes.Line(1, 1, statsWidthHighRes - 2, 1, argbBorder); //--- Draw inner top
      canvasStatsHighRes.Line(statsWidthHighRes - 2, 1, statsWidthHighRes - 2, heightHighRes - 2, argbBorder); //--- Draw inner right
      canvasStatsHighRes.Line(statsWidthHighRes - 2, heightHighRes - 2, 1, heightHighRes - 2, argbBorder); //--- Draw inner bottom
      canvasStatsHighRes.Line(1, heightHighRes - 2, 1, 1, argbBorder);     //--- Draw inner left
   }

   color labelColor = GetStatsLabelColor();   //--- Get label color
   color valueColor = GetStatsValueColor();   //--- Get value color
   color headerColor = GetStatsHeaderColor(); //--- Get header color

   int yPos = 20 * supersamplingFactor;       //--- Scaled Y position for first header
   string headerText = "Account Stats";       //--- Set header text
   int fontSizeHigh = StatsHeaderFontSize * supersamplingFactor; //--- Scaled font
   canvasStatsHighRes.FontSet("Arial Bold", fontSizeHigh);       //--- Set header font
   int textW = canvasStatsHighRes.TextWidth(headerText);         //--- Get text width
   int textH = canvasStatsHighRes.TextHeight(headerText);        //--- Get text height
   int pad = 5 * supersamplingFactor;                            //--- Scaled padding
   int rectX = (statsWidthHighRes - textW) / 2 - pad;            //--- Compute rect X
   int rectY = yPos - pad / 2;              //--- Compute rect Y
   int rectW = textW + 2 * pad;             //--- Compute rect width
   int rectH = textH + pad;                 //--- Compute rect height
   uchar alpha = (uchar)(255 * (StatsHeaderBgOpacityPercent / 100.0)); //--- Compute alpha
   uint argbHeaderBg = ColorToARGB(GetStatsHeaderColor(), alpha);      //--- Convert bg to ARGB

   color header_border_color = GetStatsHeaderColor();                  //--- Significant color
   uint argbHeaderBorder = ColorToARGB(header_border_color, 255);      //--- Convert border to ARGB
   FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect
   DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 1 * supersamplingFactor); //--- Draw border

   int yPosSecond = 120 * supersamplingFactor;        //--- Scaled Y for second header
   headerText = "Current Bar Stats";                  //--- Set bar header
   textW = canvasStatsHighRes.TextWidth(headerText);  //--- Get width
   textH = canvasStatsHighRes.TextHeight(headerText); //--- Get height
   rectX = (statsWidthHighRes - textW) / 2 - pad;     //--- Compute rect X
   rectY = yPosSecond - pad / 2;                      //--- Compute rect Y
   rectW = textW + 2 * pad;                           //--- Compute rect width
   rectH = textH + pad;                               //--- Compute rect height

   FillRoundedRectangle(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBg); //--- Fill inner rect
   DrawRoundedRectangleBorderHiRes(canvasStatsHighRes, rectX, rectY, rectW, rectH, StatsHeaderBgRadius * supersamplingFactor, argbHeaderBorder, 2 * supersamplingFactor); //--- Draw border

   DownsampleCanvas(canvasStats, canvasStatsHighRes); //--- Downsample

   canvasStats.FontSet("Arial Bold", StatsHeaderFontSize); //--- Set header font
   uint argbHeader = ColorToARGB(headerColor, 255); //--- Convert header to ARGB
   canvasStats.TextOut((currentWidth / 2) / 2, 20, "Account Stats", argbHeader, TA_CENTER); //--- Draw first header text

   canvasStats.TextOut((currentWidth / 2) / 2, 120, "Current Bar Stats", argbHeader, TA_CENTER); //--- Draw second header text

   canvasStats.FontSet("Arial Bold", StatsFontSize); //--- Set stats font
   uint argbLabel = ColorToARGB(labelColor, 255); //--- Convert label to ARGB
   uint argbValue = ColorToARGB(valueColor, 255); //--- Convert value to ARGB

   int yPosDisplay = 50;                // Name at 50
   canvasStats.TextOut(10, yPosDisplay, "Name:", argbLabel, TA_LEFT); //--- Draw name label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, AccountInfoString(ACCOUNT_NAME), argbValue, TA_RIGHT); //--- Draw name value
   yPosDisplay += 20;                   // Balance at 70

   canvasStats.TextOut(10, yPosDisplay, "Balance:", argbLabel, TA_LEFT); //--- Draw balance label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2), argbValue, TA_RIGHT); //--- Draw balance value
   yPosDisplay += 20;                   // Equity at 90

   canvasStats.TextOut(10, yPosDisplay, "Equity:", argbLabel, TA_LEFT); //--- Draw equity label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2), argbValue, TA_RIGHT); //--- Draw equity value
   yPosDisplay += 30;                   // Second header at 120, then labels

   yPosDisplay = 150;                   // Open at 150 (120 +30)
   canvasStats.TextOut(10, yPosDisplay, "Open:", argbLabel, TA_LEFT); //--- Draw open label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iOpen(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw open value
   yPosDisplay += 20;                   // High at 170

   canvasStats.TextOut(10, yPosDisplay, "High:", argbLabel, TA_LEFT); //--- Draw high label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iHigh(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw high value
   yPosDisplay += 20;                   // Low at 190

   canvasStats.TextOut(10, yPosDisplay, "Low:", argbLabel, TA_LEFT); //--- Draw low label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iLow(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw low value
   yPosDisplay += 20;                   // Close at 210

   canvasStats.TextOut(10, yPosDisplay, "Close:", argbLabel, TA_LEFT); //--- Draw close label
   canvasStats.TextOut((currentWidth / 2) - 10, yPosDisplay, DoubleToString(iClose(_Symbol, _Period, 0), _Digits), argbValue, TA_RIGHT); //--- Draw close value

   canvasStats.Update();                //--- Update stats canvas
}

Im Statistik-Panel beginnen wir damit, die hochauflösende Statistik-Zeichenfläche mit der auf 0 gesetzten Methode Erase zu löschen, um sie für eine neue Darstellung vorzubereiten, und berechnen die skalierten Abmessungen „statsWidthHighRes“ und „heightHighRes“, indem wir die ursprüngliche Breite (die Hälfte von „currentWidth“) und Höhe der Statistik mit dem „supersamplingFactor“ multiplizieren, um die Detailgenauigkeit zu verbessern. Wenn ein Hintergrund über „UseBackground“ aktiviert wird und das Array „bg_pixels_stats“ der ursprünglichen Größe entspricht, deklarieren wir ein hochauflösendes Hintergrund-Array „bg_pixels_stats_high“, passen dessen Größe an die skalierten Abmessungen an, skalieren die ursprünglichen Pixel mithilfe der Funktion „ScaleImage“, die bikubische Interpolation verwendet, um die Glätte bei der Vergrößerung zu bewahren, und durchlaufen anschließend jedes Pixel in einer Schleife, um es mit der Methode PixelSet auf der hochauflösenden Zeichenfläche zu setzen.

Wenn „StatsBackgroundMode“ nicht „NoColor“ ist, wenden wir einen vertikalen Farbverlauf oder einen einfarbigen Hintergrund an, indem wir die hochauflösenden Zeilen durchlaufen, einen Überblendungsfaktor basierend auf der y-Position berechnen, im Farbverlaufsmodus die aktuelle Farbe mit „InterpolateColor“ ermitteln, diese unter Berücksichtigung der Deckkraft aus „BackgroundOpacity“ in ARGB konvertieren und jedes Pixel über die gesamte Zeile hinweg mit „BlendPixels“ überblenden, nachdem wir den vorhandenen Wert mit PixelGet abgerufen haben, um transparente Überlagerungen zu erzielen.

Für Rahmen in Hintergrundmodi berechnen wir die reduzierte Deckkraft und den Abdunkelungsfaktor anhand von Eingabewerten wie „BorderOpacityPercentReduction“ und „BorderDarkenPercent“. Anschließend werden bei vertikalen Rahmen die Grundfarbe zeilenweise abgedunkelt und die ARGB-Pixel an den linken und rechten Rändern einschließlich der inneren Bereiche gesetzt. In ähnlicher Weise verwenden wir bei den horizontalen Rahmen oben und unten feste Faktoren, um ganze Zeilen abzudunkeln und zu füllen, wodurch subtile, verlaufsangepasste Rahmen ohne harte Kanten entstehen. In Fällen ohne Hintergrund wandeln wir die Rahmenfarbe in ARGB um und zeichnen die äußeren und inneren Linien mithilfe der Methode Line für die obere, rechte, untere und linke Seite auf der hochauflösenden Zeichenfläche auf, um die Konsistenz zu gewährleisten.

Wir rufen themenbezogene Farben mit Funktionen wie „GetStatsLabelColor“ ab, skalieren dann für die Überschrift „Account Stats“ die y-Position „yPos“ und die Schriftgröße auf hohe Auflösung, stellen die Schrift auf fett ein, berechnen die Abmessungen des zentrierten Rechtecks mit skaliertem Abstand, füllen den Hintergrund mit „FillRoundedRectangle“ unter Verwendung von ARGB mit geringer Deckkraft und zeichnen dessen Rahmen über „DrawRoundedRectangleBorderHiRes“ mit einfacher, entsprechend hochskalierter Rahmenstärke. Wir wiederholen dies für die Überschrift „Current Bar Stats“ bei skaliertem „yPosSecond“ mit einer Rahmenstärke von 2, um Abwechslung zu schaffen.

Nach dem Rendern in hoher Auflösung rufen wir „DownsampleCanvas“ auf, um die Pixel auf die Standard-Zeichenfläche von „canvasStats“ herunterzurechnen und so Ergebnisse mit Kantenglättung zu erzielen. Anschließend legen wir auf der Standard-Zeichenfläche eine fette Schrift in Originalgröße fest, um zentrierte Kopfzeilentexte mit ARGB-Farben zu zeichnen. Wir wechseln zur Schrift für die Statistik, konvertieren die Farben der Beschriftungen und Werte in ARGB und zeichnen die Beschriftungen und Werte der Kontoinformationen (Name, Kontostand, Equity) an inkrementellen Anzeigepositionen „yPosDisplay“ mithilfe von TextOut mit links- und rechtsbündiger Ausrichtung, wobei die Daten über die Funktionen AccountInfoString und AccountInfoDouble abgerufen werden. Im weiteren Verlauf der Kennzahlen des aktuellen Balkens zeichnen wir OHLC-Bezeichnungen und -Werte (Eröffnungskurs, Höchstkurs, Tiefstkurs, Schlusskurs) an weiter inkrementierten yPosDisplay-Positionen ein und ermitteln dabei die Kurse mit iOpen, iHigh, iLow und iClose für das aktuelle Symbol und den aktuellen Zeitraum, die mit der Funktion DoubleToString auf eine bestimmte Anzahl von Stellen formatiert werden. Abschließend rufen wir die Funktion „Update“ für „canvasStats“ auf, um die Anzeige zu aktualisieren und sicherzustellen, dass sich alle hochauflösenden Verbesserungen in flüssigen, professionellen Darstellungen im Statistikbereich niederschlagen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

VERBESSERTE, ABGERUNDETE STATISTIKÜBERSCHRIFTEN, ABGERUNDETE RECHTECKE

Nachdem das erledigt ist, werden wir nun auch das Textpanel auf die gleiche Weise aktualisieren, um ihm ein modernes Aussehen zu verleihen.

//+------------------------------------------------------------------+
//| Update text on canvas                                            |
//+------------------------------------------------------------------+
void UpdateTextOnCanvas() {             // Render text elements
   canvasTextHighRes.Erase(0);          //--- Clear high-res text canvas

   int textWidthHighRes = canvasText.Width() * supersamplingFactor; //--- Scaled width
   int textHeightHighRes = canvasText.Height() * supersamplingFactor; //--- Scaled height

   color text_bg = is_dark_theme ? text_bg_dark : text_bg_light; //--- Get bg color
   uint argb_bg = ColorToARGB(text_bg, (uchar)(255 * (TextBackgroundOpacityPercent / 100.0))); //--- Convert bg to ARGB
   canvasTextHighRes.FillRectangle(0, 0, textWidthHighRes - 1, textHeightHighRes - 1, argb_bg); //--- Fill background

   uint argbBorder = ColorToARGB(GetBorderColor(), 255); //--- Convert border to ARGB
   canvasTextHighRes.Line(0, 0, textWidthHighRes - 1, 0, argbBorder); //--- Draw top border
   canvasTextHighRes.Line(textWidthHighRes - 1, 0, textWidthHighRes - 1, textHeightHighRes - 1, argbBorder); //--- Draw right border
   canvasTextHighRes.Line(textWidthHighRes - 1, textHeightHighRes - 1, 0, textHeightHighRes - 1, argbBorder); //--- Draw bottom border
   canvasTextHighRes.Line(0, textHeightHighRes - 1, 0, 0, argbBorder); //--- Draw left border

   int padding = 10 * supersamplingFactor; //--- Scaled padding
   int textAreaX = padding;             //--- Set area X
   int textAreaY = 0;                   //--- Set area Y
   int textAreaWidth = textWidthHighRes - padding * 2; //--- Compute area width
   int textAreaHeight = textHeightHighRes; //--- Set area height
   string font = "Calibri";               //--- Set font
   int fontSize = TextFontSize * supersamplingFactor;  // Scaled font size
   canvasTextHighRes.FontSet(font, fontSize); //--- Apply font
   int lineHeight = canvasTextHighRes.TextHeight("A"); //--- Get line height
   text_adjustedLineHeight = (lineHeight + 3) / supersamplingFactor; //--- Adjust line height (display scale)

   text_visible_height = textAreaHeight / supersamplingFactor; //--- Visible height (display scale)
   static string wrappedLines[];        //--- Declare wrapped lines
   static color wrappedColors[];        //--- Declare wrapped colors
   static bool wrapped = false;         //--- Track wrapped state
   bool need_scroll = false;            //--- Set scroll need
   int reserved_width = 0;              //--- Set reserved width
   if (!wrapped) {                      //--- Check not wrapped
      WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Wrap at display scale
      wrapped = true;                   //--- Set wrapped flag
   }
   int numLines = ArraySize(wrappedLines); //--- Get line count
   text_total_height = numLines * text_adjustedLineHeight; //--- Compute total height
   need_scroll = text_total_height > text_visible_height; //--- Check need scroll
   if (need_scroll) {                   //--- Handle scroll needed
      reserved_width = text_track_width * supersamplingFactor; //--- Reserve scaled width
      textAreaWidth -= reserved_width;  //--- Adjust area width
      WrapText(text_usage_text, font, fontSize / supersamplingFactor, textAreaWidth / supersamplingFactor, wrappedLines, wrappedColors); //--- Rewrap text
      numLines = ArraySize(wrappedLines); //--- Update line count
      text_total_height = numLines * text_adjustedLineHeight; //--- Update total height
   }
   text_max_scroll = MathMax(0, text_total_height - text_visible_height) * smoothness_factor; //--- Compute max scroll
   text_scroll_visible = need_scroll;   //--- Set scroll visible
   text_scroll_pos = MathMax(0, MathMin(text_scroll_pos, text_max_scroll)); //--- Clamp scroll pos
   if (text_scroll_visible) {           //--- Check scroll visible
      int scrollbar_y = 0;              //--- Set scrollbar Y
      int scrollbar_height = textAreaHeight; //--- Set scrollbar height
      int scroll_area_height = scrollbar_height - 2 * (text_button_size * supersamplingFactor); //--- Scaled area height
      text_slider_height = TextCalculateSliderHeight(); //--- Compute slider height
      int scrollbar_x = textWidthHighRes - (text_track_width * supersamplingFactor); //--- Scaled scrollbar X
      color leader_color = is_dark_theme ? text_leader_color_dark : text_leader_color_light; //--- Get leader color
      uint argb_leader = ColorToARGB(leader_color, 255); //--- Convert to ARGB
      canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + scrollbar_height - 1, argb_leader); //--- Fill leader

      int slider_y = scrollbar_y + (text_button_size * supersamplingFactor) + (int)(((double)text_scroll_pos / text_max_scroll) * (scroll_area_height - (text_slider_height * supersamplingFactor))); //--- Scaled slider Y

      if (text_scroll_area_hovered) {   //--- Check area hovered
         color button_bg = is_dark_theme ? text_button_bg_dark : text_button_bg_light; //--- Get button bg
         color button_bg_hover = is_dark_theme ? text_button_bg_hover_dark : text_button_bg_hover_light; //--- Get hover bg
         color up_bg;
         if (ShowUpDownButtons) {
            up_bg = text_scroll_up_hovered ? button_bg_hover : button_bg;
         } else {
            up_bg = leader_color; // Blend with track, no hover
         }
         uint argb_up_bg = ColorToARGB(up_bg, (uchar)255); //--- Convert up bg
         canvasTextHighRes.FillRectangle(scrollbar_x, scrollbar_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, scrollbar_y + (text_button_size * supersamplingFactor) - 1, argb_up_bg); //--- Fill up button
         color arrow_color = is_dark_theme ? text_arrow_color_dark : text_arrow_color_light; //--- Get arrow color
         color arrow_color_disabled = is_dark_theme ? text_arrow_color_disabled_dark : text_arrow_color_disabled_light; //--- Get disabled arrow
         color arrow_color_hover = is_dark_theme ? text_arrow_color_hover_dark : text_arrow_color_hover_light; //--- Get hover arrow
         color up_arrow = (text_scroll_pos == 0) ? arrow_color_disabled : (text_scroll_up_hovered ? arrow_color_hover : arrow_color); //--- Get up arrow color
         uint argb_up_arrow = ColorToARGB(up_arrow, (uchar)255); //--- Convert up arrow

         // Draw up rounded triangle
         int arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X
         double base_width = text_button_size * (TriangleBaseWidthPercent / 100.0) * supersamplingFactor; //--- Compute base width
         int tri_height = (int)(base_width * (TriangleHeightPercent / 100.0)); //--- Compute height
         int arrow_y = scrollbar_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y
         DrawRoundedTriangleArrow(canvasTextHighRes, arrow_x, arrow_y, (int)base_width, tri_height, true, argb_up_arrow); //--- Up arrow

         int down_y = scrollbar_y + scrollbar_height - (text_button_size * supersamplingFactor); //--- Compute down Y
         color down_bg;
         if (ShowUpDownButtons) {
            down_bg = text_scroll_down_hovered ? button_bg_hover : button_bg;
         } else {
            down_bg = leader_color; // Blend with track, no hover
         }
         uint argb_down_bg = ColorToARGB(down_bg, (uchar)255); //--- Convert down bg
         canvasTextHighRes.FillRectangle(scrollbar_x, down_y, scrollbar_x + (text_track_width * supersamplingFactor) - 1, down_y + (text_button_size * supersamplingFactor) - 1, argb_down_bg); //--- Fill down button
         color down_arrow = (text_scroll_pos >= text_max_scroll) ? arrow_color_disabled : (text_scroll_down_hovered ? arrow_color_hover : arrow_color); //--- Get down arrow color
         uint argb_down_arrow = ColorToARGB(down_arrow, (uchar)255); //--- Convert down arrow

         // Draw down rounded triangle
         int down_arrow_x = scrollbar_x + (text_track_width * supersamplingFactor) / 2; //--- Center X
         int down_arrow_y = down_y + ((text_button_size * supersamplingFactor) - tri_height) / 2; //--- Centered Y
         DrawRoundedTriangleArrow(canvasTextHighRes, down_arrow_x, down_arrow_y, (int)base_width, tri_height, false, argb_down_arrow); //--- Down arrow

         int slider_x = scrollbar_x + (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider X
         int slider_w = (text_track_width * supersamplingFactor) - 2 * (text_scrollbar_margin * supersamplingFactor); //--- Scaled slider width
         int cap_radius = slider_w / 2; //--- Compute cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg
         color slider_bg_hover_color = is_dark_theme ? text_slider_bg_hover_dark : text_slider_bg_hover_light; //--- Get hover bg
         color slider_bg = text_scroll_slider_hovered || text_movingStateSlider ? slider_bg_hover_color : slider_bg_color; //--- Determine slider bg
         uint argb_slider = ColorToARGB(slider_bg, (uchar)255); //--- Convert slider to ARGB

         canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc
         canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap

         canvasTextHighRes.Arc(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc
         canvasTextHighRes.FillCircle(slider_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap

         canvasTextHighRes.FillRectangle(slider_x, slider_y + cap_radius, slider_x + slider_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill slider body
      } else {                          //--- Handle thin scrollbar
         int thin_w = text_scrollbar_thin_width * supersamplingFactor; //--- Scaled thin width
         int thin_x = scrollbar_x + ((text_track_width * supersamplingFactor) - thin_w) / 2; //--- Compute thin X
         int cap_radius = thin_w / 2;   //--- Compute cap radius
         color slider_bg_color = is_dark_theme ? text_slider_bg_dark : text_slider_bg_light; //--- Get slider bg
         uint argb_slider = ColorToARGB(slider_bg_color, (uchar)255); //--- Convert to ARGB

         canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + cap_radius, cap_radius, cap_radius, DegreesToRadians(180), DegreesToRadians(360), argb_slider); //--- Draw top arc
         canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + cap_radius, cap_radius, argb_slider); //--- Fill top cap

         canvasTextHighRes.Arc(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, cap_radius, DegreesToRadians(0), DegreesToRadians(180), argb_slider); //--- Draw bottom arc
         canvasTextHighRes.FillCircle(thin_x + cap_radius, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, cap_radius, argb_slider); //--- Fill bottom cap

         canvasTextHighRes.FillRectangle(thin_x, slider_y + cap_radius, thin_x + thin_w, slider_y + (text_slider_height * supersamplingFactor) - cap_radius, argb_slider); //--- Fill thin body
      }
   }

   DownsampleCanvas(canvasText, canvasTextHighRes); //--- Downsample

   color text_base = is_dark_theme ? text_base_dark : text_base_light; //--- Get base color
   canvasText.FontSet("Calibri", TextFontSize);     //--- Normal font
   for (int line = 0; line < numLines; line++) { //--- Loop lines
      string lineText = wrappedLines[line]; //--- Get line text
      if (StringLen(lineText) == 0) continue; //--- Skip empty
      color lineColor = wrappedColors[line]; //--- Get line color
      if (is_dark_theme) lineColor = (lineColor == clrWhite) ? clrWhite : LightenColor(lineColor, 1.5); //--- Adjust dark color
      else lineColor = (lineColor == clrWhite) ? clrBlack : DarkenColor(lineColor, 0.7); //--- Adjust light color
      int line_y = (textAreaY / supersamplingFactor) + line * text_adjustedLineHeight - (text_scroll_pos/smoothness_factor); //--- Compute line Y (display scale)
      if (line_y + text_adjustedLineHeight < 0 || line_y > text_visible_height) continue; //--- Skip out of view
      if (IsHeading(lineText) ) {        //--- Check heading
         canvasText.FontSet("Calibri Bold", TextFontSize); //--- Set bold font
         lineColor = clrDodgerBlue;     //--- Set heading color
      } else canvasText.FontSet("Calibri", TextFontSize); //--- Set normal font
      uint argbText = ColorToARGB(lineColor, 255); //--- Convert to ARGB
      canvasText.TextOut(textAreaX / supersamplingFactor, line_y, lineText, argbText, TA_LEFT); //--- Draw line text
   }

   canvasText.Update();                 //--- Update text canvas
}

In der Funktion der Text-Zeichenfläche löschen wir zunächst die hochauflösende Text-Zeichenfläche mit der auf 0 gesetzten Methode Erase und berechnen die skalierten Abmessungen „textWidthHighRes“ und „textHeightHighRes“, indem wir die Standardbreite und -höhe des Canvases mit dem „supersamplingFactor“ multiplizieren, um eine detaillierte Darstellung zu erzielen. Wir wählen die Hintergrundfarbe „text_bg“ basierend auf dem Theme „is_dark_theme“ aus, wandeln sie mit der Deckkraft aus „TextBackgroundOpacityPercent“ in ARGB um und füllen das gesamte hochauflösende Rechteck mit „FillRectangle“ aus; anschließend zeichnen wir die Ränder mit den „Line“-Methoden, wobei wir für die obere, rechte, untere und linke Kante ARGB-Werte aus „GetBorderColor“ verwenden.

Wir skalieren den Abstand auf hohe Auflösung mit „supersamplingFactor“, legen die Begrenzungen des Textbereichs mit „textAreaX“, „textAreaY“, „textAreaWidth“ und „textAreaHeight“ fest, ändern die Schriftart auf „Calibri“ mit einer skalierten „fontSize“ aus „TextFontSize“, ermitteln die Zeilenhöhe für „A“ und passen „text_adjustedLineHeight“ wieder an den Anzeigemaßstab an, indem wir durch „supersamplingFactor“ dividieren. Wir berechnen „text_visible_height“ als die skalierte Bereichshöhe geteilt durch „supersamplingFactor“ und verwenden statische Arrays „wrappedLines“ und „wrappedColors“ mit einem „wrapped“-Flag, um den Textumbruch zwischenzuspeichern, und rufen „WrapText“ auf, falls noch kein Umbruch stattgefunden hat, wobei die Parameter für die Anzeigeskalierung (Schriftgröße und Breite geteilt durch den Faktor) verwendet werden, um „text_usage_text“ in Zeilen mit Farben aufzuteilen.

Wir ermitteln die Anzahl der Zeilen „numLines“ und die Gesamthöhe „text_total_height“ als Produkt aus Zeilenanzahl und angepasster Höhe, prüfen anhand der sichtbaren Höhe, ob ein Bildlauf erforderlich ist („need_scroll“), und reservieren in diesem Fall eine skalierte Breite „reserved_width“ für die Bildlaufleiste, passen die Breite des Bereichs an, führen einen Textumbruch durch und aktualisieren die Zählwerte und Höhen. Wir berechnen „text_max_scroll“ als die überschüssige Höhe multipliziert mit dem „smoothness_factor“, um eine feinere Scrollsteuerung zu ermöglichen, setzen „text_scroll_visible“ auf das entsprechende Flag, begrenzen „text_scroll_pos“ auf einen Wert zwischen 0 und max, und, falls sichtbar, positionieren wir die Bildlaufleiste rechts mit skalierter Spurbreite und füllen den Track-Hintergrund mit dem themenbasierten „argb_leader“ aus „leader_color“.

Für die Schiebereglerposition „slider_y“ skalieren wir die Schaltflächengröße und die Höhe des Bereichs, berechnen die Höhe mit „TextCalculateSliderHeight“ (ursprünglicher Maßstab) und richten, wenn der Mauszeiger über dem Bereich schwebt („text_scroll_area_hovered“), bedingt die Hintergründe der Auf- und Ab-Schaltflächen basierend auf „ShowUpDownButtons“ ein — dabei werden die Hover-Farben verwendet, sofern angezeigt, andernfalls erfolgt eine Überblendung mit der Führungslinie — die Rechtecke werden gefüllt, die Pfeilfarben werden unter Berücksichtigung des deaktivierten oder Hover-Zustands festgelegt, und es werden abgerundete Dreieckspfeile nach oben und unten mit „DrawRoundedTriangleArrow“ gezeichnet, wobei die berechnete Basisbreite aus „TriangleBaseWidthPercent“, die Höhe aus „TriangleHeightPercent“, zentrierte Positionen sowie die Richtung „isUp“ oder „false“ verwendet werden.

Anschließend zeichnen wir den Schieberegler mit dem skalierten Rand „slider_x“, der Breite „slider_w“ und dem Radius der abgerundeten Enden, wobei wir den Hintergrund je nach Hover- oder Bewegungszustand als „text_scroll_slider_hovered“ oder „text_movingStateSlider“ auswählen, die oberen und unteren Bögen mit „Arc“ zeichnen, die Endpunkte mit „FillCircle“ und den Körper mit „FillRectangle“ füllen; für den schmalen Modus ohne Hover zeichnen wir auf ähnliche Weise einen schmaleren Schieberegler, der in der Spur zentriert ist. Nach dem hochauflösenden Rendering führen wir mithilfe von „DownsampleCanvas“ eine Abtastung auf die Standardgröße „canvasText“ durch, um eine kantenglättete Ausgabe zu erzielen. Wir wählen die themenbasierte Textfarbe „text_base“ aus, legen die Schriftart „Calibri“ in der ursprünglichen „TextFontSize“ fest, durchlaufen die Zeilenumbrüche und überspringen dabei leere Zeilen, passen die Farben mit „LightenColor“ oder „DarkenColor“ an das Thema an, berechnen die y-Position „line_y“ im Anzeigemaßstab unter Abzug des geglätteten Bildlaufs „text_scroll_pos / smoothness_factor“, überspringen Zeilen, die nicht im Sichtbereich liegen, setzen Überschriften in „Calibri Bold“ und die Farbe auf „clrDodgerBlue“, konvertieren in ARGB und zeichnen jede mit TextOut an den skalierten x- und y-Koordinaten. Abschließend rufen wir Update für „canvasText“ auf, um das überarbeitete, visuell glattere Textpanel mit verbessertem Bildlauf und verbesserter Optik anzuzeigen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

SUPER-SAMPLED-SCROLLLEISTE

Wir können sehen, dass die Bildlaufleiste dank Supersampling moderner und sauberer dargestellt wird, wodurch wir unser Ziel erreicht haben. Nun bleibt nur noch, die Praxistauglichkeit des Systems zu testen, was im vorangegangenen Abschnitt behandelt wurde.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung in Form eines einzigen GIF-Bilds.

BACKTEST-GIF


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir das Canvas-Dashboard in MQL5 durch Anti-Aliasing-Verfahren und hochauflösendes Rendering mittels Supersampling optimiert haben, um glattere Grafiken und Elemente zu erzielen. Wir haben hochauflösende Zeichenflächen für Statistik- und Textpanels sowie präzise Zeichenfunktionen für abgerundete Rahmen, Rechtecke und Pfeile hinzugefügt und damit die visuelle Gesamtqualität verbessert. Dank dieser verbesserten Kantenglättung und der hochauflösenden Darstellung sind Sie bestens gerüstet, um übersichtlichere und professionellere Trading-Dashboards zu erstellen, die sich im weiteren Verlauf Ihrer Entwicklung individuell anpassen lassen. Viel Spaß beim Handeln!

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

Beigefügte Dateien |
Python-MetaTrader 5-Strategietester (Teil 05): Strategietests mit mehreren Symbolen und Zeitrahmen Python-MetaTrader 5-Strategietester (Teil 05): Strategietests mit mehreren Symbolen und Zeitrahmen
Dieser Artikel stellt einen MetaTrader 5-kompatiblen Backtesting-Workflow vor, der sich auf verschiedene Symbole und Zeitrahmen hinweg skaliert. Wir nutzen den HistoryManager, um die Datenerfassung zu parallelisieren, Kursbalken und Ticks aus allen Zeitrahmen zu synchronisieren und symbolisolierte OnTick-Handler in Threads auszuführen. Sie erfahren, wie sich Modellierungsmodi auf Geschwindigkeit und Genauigkeit auswirken, wann Sie sich auf Terminaldaten verlassen können, wie Sie den I/O-Aufwand durch ereignisgesteuerte Aktualisierungen reduzieren und wie Sie einen vollständigen Multiwährungs-Trading-Roboter erstellen.
Optionshandel ohne Optionen (Teil 1): Grundlagen und Nachbildung mittels des Basiswerte Optionshandel ohne Optionen (Teil 1): Grundlagen und Nachbildung mittels des Basiswerte
Der Artikel beschreibt eine Variante der Options-Nachbildung über einen Basiswert, die in der Programmiersprache MQL5 implementiert ist. Die Vor- und Nachteile des gewählten Ansatzes werden anhand des FORTS-Futuresmarkts der Moskauer Börse MOEX und der Kryptobörse Bybit mit realen börsengehandelten Optionen verglichen.
Die MQL5-Standardbibliothek im Überblick (Teil 8): Ein hybrides Handelsjournal mit CFileTxt Die MQL5-Standardbibliothek im Überblick (Teil 8): Ein hybrides Handelsjournal mit CFileTxt
In diesem Artikel befassen wir uns mit den Dateiverarbeitungsklassen der MQL5-Standardbibliothek, um ein robustes Reporting-Modul zu entwickeln, das automatisch Excel-kompatible CSV-Dateien generiert. Dabei unterscheiden wir klar zwischen manuell ausgeführten Trades und algorithmisch ausgeführten Orders und schaffen damit die Grundlage für ein zuverlässiges, auditierbares Trade-Reporting.
Erkennung und Klassifizierung fraktaler Muster mit maschinellem Lernen Erkennung und Klassifizierung fraktaler Muster mit maschinellem Lernen
In diesem Artikel werden wir uns mit dem spannenden Thema der fraktalen Analyse und der Marktprognose mithilfe von maschinellem Lernen befassen. Dies sind nur die ersten Schritte auf dem Weg zur Erforschung der vielfältigen fraktalen Strukturen, die sich in Finanzkurscharts bilden. Wir werden die Korrelation nutzen, um Muster zu erkennen, und den CatBoost-Algorithmus, um diese Muster zu klassifizieren.