English Русский
preview
MQL5-Handelswerkzeuge (Teil 23): Kameragesteuerte 3D-Diagramme mit DirectX-Unterstützung für Einblicke in die Verteilungen

MQL5-Handelswerkzeuge (Teil 23): Kameragesteuerte 3D-Diagramme mit DirectX-Unterstützung für Einblicke in die Verteilungen

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

Einführung

Sie verfügen zwar über ein Tool zur grafischen Darstellung der Binomialverteilung in zwei Dimensionen, doch ohne eine auf der Tiefe basierende Visualisierung lassen sich Muster in der Wahrscheinlichkeitsmassenfunktion (PMF) möglicherweise schwerer erkennen – die Überlappungen der Balken wirken flach, Häufigkeitsunterschiede verlieren an räumlichem Kontrast, und der Wechsel zwischen analytischen Perspektiven erfordert einen Neustart statt einer einfachen Umschaltung. Dieser Artikel richtet sich an Entwickler von MetaQuotes Language 5 (MQL5) sowie an algorithmische Trader, die statistische Visualisierungstools um interaktive dreidimensionale Darstellungen erweitern möchten, um tieferes Verständnis probabilistischer Zusammenhänge zu gewinnen.

In unserem vorherigen Artikel (Teil 22) haben wir ein MQL5-Diagrammtool erstellt, um die Binomialverteilung mit einem Histogramm aus simulierten Stichproben und der theoretischen Wahrscheinlichkeitsmassenfunktion (PMF) auf einer interaktiven Zeichenfläche zu visualisieren. In Teil 23 integrieren wir Direct3D in den MQL5-Viewer für Binomialverteilung und ermöglichen so umschaltbare 2D-/3D-Modi sowie die Kamerasteuerung für Drehung, Zoom und automatische Anpassung. Der Artikel zeigt, wie man 3D-Histogrammbalken mit Bodenebene und Achsen darstellt, die PMF-Kurve projiziert und dabei die 2D-Statistiken, die Legende und die thematische Gestaltung beibehält. Außerdem werden die klassenbasierte Architektur, Mausinteraktionen, Echtzeit-Aktualisierungen sowie die Parameteranpassung zur Verbesserung der Analyse von Häufigkeiten und der PMF-Form behandelt. Wir werden folgende Themen behandeln:

  1. Die Architektur eines Frameworks für 3D-Visualisierung mit DirectX verstehen
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende verfügen Sie über ein funktionsfähiges MQL5-Tool mit 3D-Funktionen zur Analyse der Binomialverteilung, das sich ganz nach Ihren Wünschen anpassen lässt – legen wir los!


Die Architektur eines Frameworks für 3D-Visualisierung mit DirectX verstehen

Das DirectX-3D-Visualisierungs-Framework in MQL5 nutzt hardwarebeschleunigte Grafik zur Darstellung komplexer 3D-Szenen in Charts und ist in das Canvas-System integriert, um einen nahtlosen Wechsel zwischen 2D- und 3D-Modus sowie interaktive Steuerelemente zu ermöglichen. Es nutzt DirectX für die effiziente Darstellung von 3D-Objekten wie Boxen für Histogrammbalken, Ebenen für Bodenebenen und Linien für Achsen und steuert dabei Kamerapositionen, Beleuchtung und Projektionen, um in der Datenvisualisierung Tiefe und Perspektive zu erzeugen. Diese Architektur unterstützt dynamische Benutzerinteraktionen wie Drehen, Zoomen und automatische Anpassung und eignet sich daher ideal für die Analyse mehrdimensionaler Daten wie Verteilungen im Handelskontext, wo die visuelle Tiefe Muster hervorhebt, die in 2D nicht erkennbar sind.

Wir beabsichtigen, das Tool zur Darstellung von 2D-Binomialverteilungen weiterzuentwickeln, indem wir einen 3D-Modus hinzufügen, der die Histogrammbalken dreidimensional visualisiert, Bodenebenen und farbige Achsen zur Orientierung einbezieht und die Manipulation der Kamera ermöglicht, um Wahrscheinlichkeitsmassfunktionen und Häufigkeiten besser untersuchen zu können. Der Entwurf sieht eine klassenbasierte Struktur vor, die die Erstellung der Zeichenfläche, die Initialisierung von 3D-Objekten, das Laden von Daten für Simulationen sowie ereignisgesteuerte Aktualisierungen für Echtzeit-Reaktionsfähigkeit abdeckt. Wir werden eine Visualizer-Klasse definieren, die die 2D- und 3D-Rendering-Logik kapselt, 3D-Elemente mithilfe von Box-Primitiven erstellt, Projektions- und Ansichtsmatrizen für die Kamerasteuerung einrichtet und die Modusumschaltung mit interaktiven Funktionen wie Ziehen, Größenänderung und Zoomen per Mausrad integriert, um schließlich ein Werkzeug für eine eingehende probabilistische Analyse in Handelsszenarien bereitzustellen. Kurz gesagt: Dieses Framework wandelt zweidimensionale Datendiagramme in interaktive 3D-Modelle um, um tiefere Einblicke zu ermöglichen. So soll das Ergebnis aussehen.

DIRECTX 3D-ARCHITEKTUR GIF


Implementierung in MQL5

Einbindung von Bibliotheken, Enumerationen und Eingabeparametern für die dreidimensionale Unterstützung

Bevor eine dreidimensionale Darstellung möglich ist, müssen wir die Grundlage des Programms erweitern – indem wir die richtigen Bibliotheken für die Unterstützung von einer dreidimensionalen Zeichenfläche, und DirectX-Boxen einbinden, eine Enumeration definieren, die es dem Benutzer ermöglicht, zur Laufzeit ohne Neustart zwischen dem zweidimensionalen und dem dreidimensionalen Modus zu wechseln, und die entsprechenden Eingabeparameter und Konstanten einrichten, die das Aussehen und Verhalten der dreidimensionalen Umgebung ab dem Zeitpunkt des Programmstarts steuern. Hier ist die Logik, mit der wir das erreichen.

//+------------------------------------------------------------------+
//|    Canvas Graphing PART 3.1 - Statistical Distributions (3D).mq5 |
//|                           Copyright 2026, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"
#property strict

#include <Canvas\Canvas.mqh>
#include <Canvas\Canvas3D.mqh>
#include <Canvas\DX\DXBox.mqh>
#include <Math\Stat\Binomial.mqh>
#include <Math\Stat\Math.mqh>

//+------------------------------------------------------------------+
//| Enumerations                                                     |
//+------------------------------------------------------------------+
enum ResizeDirection {
   NO_RESIZE,                                           // No resize
   RESIZE_BOTTOM_EDGE,                                  // Resize bottom edge
   RESIZE_RIGHT_EDGE,                                   // Resize right edge
   RESIZE_CORNER                                        // Resize corner
};

enum ViewModeType {
   VIEW_2D_MODE,                                        // 2D mode
   VIEW_3D_MODE                                         // 3D mode
};

//+------------------------------------------------------------------+
//| Inputs                                                           |
//+------------------------------------------------------------------+
sinput group "=== VIEW MODE SETTINGS ==="
input ViewModeType     viewMode = VIEW_2D_MODE;         // View Mode (2D or 3D)

sinput group "=== 3D VIEW SETTINGS ==="
input bool               autoFitCamera = true;          // Auto-Fit Camera on Load
input double             initialCameraDistance = 60.0;  // Initial Camera Distance (3D)
input double             initialCameraAngleX = 0.6;     // Initial Camera Angle X (3D)
input double             initialCameraAngleY = 0.8;     // Initial Camera Angle Y (3D)

sinput group "=== 3D GROUND AND AXES SETTINGS ==="
input color              groundPlaneColor = clrLightGray; // Ground Plane Color
input double             groundPlaneOpacity = 0.6;      // Ground Plane Opacity (0-1)
input float              groundPlaneWidth = 50.0;       // Ground Plane Width (X direction)
input float              groundPlaneDepth = 20.0;       // Ground Plane Depth (Z direction)
input float              groundPlaneThickness = 0.5;    // Ground Plane Thickness (Y direction)
input bool               show3DAxes = true;             // Show 3D Axes
input color              axisXColor = clrRed;           // X Axis Color
input color              axisYColor = clrGreen;         // Y Axis Color
input color              axisZColor = clrBlue;          // Z Axis Color
input float              axisLength = 20.0;             // Axis Length
input float              axisThickness = 0.1f;          // Axis Thickness

//+------------------------------------------------------------------+
//| Constants                                                        |
//+------------------------------------------------------------------+
const int MIN_CANVAS_WIDTH = 300;
const int MIN_CANVAS_HEIGHT = 200;
const int HEADER_BAR_HEIGHT = 35;
const int SWITCH_ICON_SIZE = 24;
const int SWITCH_ICON_MARGIN = 6;

Wir beginnen die Implementierung mit dem Einbinden der zusätzlich benötigten Bibliotheken: #include <Canvas\Canvas3D.mqh>, um 3D-Rendering-Funktionen zu aktivieren, und #include <Canvas\DX\DXBox.mqh> für DirectX-Box-Primitive, die in 3D-Modellen verwendet werden. Fügen wir als Nächstes eine Enumeration für Benutzeransichten hinzu. Die Enumeration ViewModeType enthält die Werte VIEW_2D_MODE und VIEW_3D_MODE, mit denen zwischen den Anzeigemodi umgeschaltet werden kann. Anschließend haben wir zur besseren Übersichtlichkeit zusätzliche Eingabeparameter eingerichtet, beginnend mit der Auswahl des Anzeigemodus über viewMode, dessen Standardwert VIEW_2D_MODE ist. Für 3D-spezifische Einstellungen stellen wir Eingabemöglichkeiten wie autoFitCamera für die automatische Kamerapositionierung sowie initialCameraDistance, initialCameraAngleX und initialCameraAngleY zur Konfiguration der anfänglichen Kameraansichten bereit.

Zudem fügen wir Eingabefelder für den 3D-Boden und die Achsen hinzu: groundPlaneColor, groundPlaneOpacity, Maße wie groundPlaneWidth, groundPlaneDepth und groundPlaneThickness, einen Schalter show3DAxes, Achsenfarben wie axisXColor sowie Längen und Stärken mit axisLength und axisThickness, wodurch eine individuelle Anpassung der 3D-Umgebung ermöglicht wird. Abschließend definieren wir zusätzliche Konstanten für Symbole wie SWITCH_ICON_SIZE und SWITCH_ICON_MARGIN für die Modusumschalt-Schaltfläche. Wir haben die spezifischen Änderungen zur Verdeutlichung hervorgehoben. Als Nächstes werden wir alle unsere globalen Variablen und Funktionen in eine Klasse umgestalten, um die Verwaltung zu vereinfachen und den Code modularer zu gestalten. Wir beginnen mit den globalen Variablen, die wir in Klassenmitgliedsvariablen umgestalten.

Definition der Klasse Visualizer und ihrer Membervariablen

Um den Code übersichtlich und skalierbar zu halten, strukturieren wir das gesamte Tool neu und fassen es in einer einzigen Klasse zusammen – dabei vereinen wir alle Zustandsvariablen, die Rendering-Logik und die Interaktionsverarbeitung in einer Klasse gebündelt. In diesem Abschnitt wird die Klassendefinition festgelegt und es werden alle Membervariablen deklariert, die zur Erfassung der Canvas-Identität, der Fenstergeometrie, der Zustände der Benutzerinteraktion, der dreidimensionalen Kameraausrichtung, der DirectX-Szenenobjekte – einschließlich Balken, Bodenebene und Achsen – sowie aller Datenarrays und statistischen Kennzahlen benötigt werden, die die Visualisierung steuern.

//+------------------------------------------------------------------+
//| Distribution visualization window class                          |
//+------------------------------------------------------------------+
class DistributionVisualizer
  {
protected:
   CCanvas3D         m_mainCanvas;           // Main 3D-capable canvas
   string            m_canvasObjectName;     // Chart object name for the canvas bitmap

   int               m_currentPositionX;     // Current X position of the canvas on chart
   int               m_currentPositionY;     // Current Y position of the canvas on chart
   int               m_currentWidth;         // Current canvas width in pixels
   int               m_currentHeight;        // Current canvas height in pixels

   bool              m_isDragging;           // True while user is dragging the canvas
   bool              m_isResizing;           // True while user is resizing the canvas
   int               m_dragStartX;           // Mouse X when drag began
   int               m_dragStartY;           // Mouse Y when drag began
   int               m_canvasStartX;         // Canvas X when drag began
   int               m_canvasStartY;         // Canvas Y when drag began
   int               m_resizeStartX;         // Mouse X when resize began
   int               m_resizeStartY;         // Mouse Y when resize began
   int               m_resizeInitialWidth;   // Canvas width at resize start
   int               m_resizeInitialHeight;  // Canvas height at resize start
   ResizeDirection   m_activeResizeMode;     // Currently active resize direction
   ResizeDirection   m_hoverResizeMode;      // Resize direction under cursor hover

   bool              m_isHoveringCanvas;      // True when mouse is over the canvas
   bool              m_isHoveringHeader;      // True when mouse is over the header bar
   bool              m_isHoveringResizeZone;  // True when mouse is in a resize grip zone
   bool              m_isHoveringSwitchIcon;  // True when mouse is over the mode switch icon
   int               m_lastMouseX;            // Last recorded mouse X coordinate
   int               m_lastMouseY;            // Last recorded mouse Y coordinate
   int               m_previousMouseButtonState; // Mouse button state on previous event

   ViewModeType      m_currentViewMode;      // Active view mode (2D or 3D)
   bool              m_are3DObjectsCreated;  // True once 3D scene objects have been built

   CDXBox            m_histogramBars[];      // Array of 3D boxes representing histogram bars
   CDXBox            m_groundPlane;          // 3D box used as the ground reference plane
   CDXBox            m_axisX;                // 3D box representing the X axis
   CDXBox            m_axisY;                // 3D box representing the Y axis
   CDXBox            m_axisZ;                // 3D box representing the Z axis

   double            m_cameraDistance;       // Distance from camera to scene origin
   double            m_cameraAngleX;         // Camera elevation angle (radians)
   double            m_cameraAngleY;         // Camera azimuth angle (radians)
   int               m_mouse3DStartX;        // Mouse X when 3D rotation drag began
   int               m_mouse3DStartY;        // Mouse Y when 3D rotation drag began
   bool              m_isRotating3D;         // True while user is rotating the 3D scene

   double            m_sampleData[];                   // Raw binomial sample values
   double            m_histogramIntervals[];           // Histogram bin center positions
   double            m_histogramFrequencies[];         // Scaled frequency per histogram bin
   double            m_theoreticalXValues[];           // X values for the theoretical PMF curve
   double            m_theoreticalYValues[];           // Y values for the theoretical PMF curve
   double            m_minDataValue;                   // Minimum value in data range
   double            m_maxDataValue;                   // Maximum value in data range
   double            m_maxFrequency;                   // Peak raw frequency across all bins
   double            m_maxTheoreticalValue;            // Peak theoretical PMF value
   bool              m_isDataLoaded;                   // True once distribution data is ready

   double            m_sampleMean;                     // Computed sample mean
   double            m_sampleStandardDeviation;        // Computed sample standard deviation
   double            m_sampleSkewness;                 // Computed sample skewness
   double            m_sampleKurtosis;                 // Computed sample excess kurtosis
   double            m_percentile25;                   // 25th percentile (Q1)
   double            m_percentile50;                   // 50th percentile (median)
   double            m_percentile75;                   // 75th percentile (Q3)
   double            m_confidenceInterval95Lower;      // Lower bound of 95% confidence interval
   double            m_confidenceInterval95Upper;      // Upper bound of 95% confidence interval
   double            m_confidenceInterval99Lower;      // Lower bound of 99% confidence interval
   double            m_confidenceInterval99Upper;      // Upper bound of 99% confidence interval
};

Hier definieren wir die Klasse DistributionVisualizer, um die gesamte Logik des Diagrammtools zu kapseln; sie dient als Fenstermanager sowohl für 2D- als auch für 3D-Visualisierungen der Binomialverteilung. Im geschützten Bereich deklarieren wir Mitglieder, beginnend mit dem CCanvas3D-Objekt m_mainCanvas für das 3D-fähige Rendering, einer Zeichenkette m_canvasObjectName als Objektbezeichner, Ganzzahlen für die aktuelle Position und die Abmessungen wie m_currentPositionX und m_currentWidth, Booleschen Werten und Ganzzahlen für Interaktionszustände wie m_isDragging, m_dragStartX und m_activeResizeMode unter Verwendung der Enumeration ResizeDirection. Wir beziehen ansichtsbezogene Variablen wie m_currentViewMode aus ViewModeType und m_are3DObjectsCreated, Arrays von CDXBox für m_histogramBars sowie einzelne Boxen für m_groundPlane, m_axisX, m_axisY und m_axisZ ein, um 3D-Elemente zu modellieren.

Zur Kamerasteuerung verwenden wir Variablen wie m_cameraDistance, m_cameraAngleX, m_cameraAngleY sowie Interaktions-Tracker wie m_mouse3DStartX und m_isRotating3D. Die Datenspeicherung umfasst Arrays für Messwerte, Histogramme und theoretische Werte wie m_sampleData und m_histogramFrequencies, Min-/Max-Tracker, ein Ladeflag m_isDataLoaded sowie statistische globale Variablen wie m_sampleMean und m_confidenceInterval95Lower. Wir werden nicht auf jede Member-Variable eingehen, da die meisten davon mit den vorherigen Versionen identisch sind und wir zur Verdeutlichung ausführliche Kommentare hinzugefügt haben. Nachdem das erledigt ist, erstellen wir einen öffentlichen Konstruktor, um unsere Variablen wie folgt zu initialisieren.

Initialisierung und Freigabe der Klasseninstanz

Nachdem die Klassenstruktur nun steht, benötigen wir einen Konstruktor, um alle Membervariablen auf sichere, vorhersehbare Startwerte zu setzen, bevor eine Darstellung oder Interaktion stattfindet, sowie einen Destruktor, um die DirectX-Ressourcen, die mit den dreidimensionalen Balken, der Bodenebene und den Achsen verknüpft sind, explizit freizugeben, wenn das Objekt nicht mehr benötigt wird – wodurch sichergestellt wird, dass das Programm sauber beendet wird, ohne dass zugewiesener GPU-Speicher zurückbleibt.

public:
   //+------------------------------------------------------------------+
   //| Initialize all member variables to safe defaults                 |
   //+------------------------------------------------------------------+
   DistributionVisualizer(void)
     {
      //--- Set canvas object name
      m_canvasObjectName = "DistCanvas";
      //--- Set initial canvas X position from input
      m_currentPositionX = initialCanvasX;
      //--- Set initial canvas Y position from input
      m_currentPositionY = initialCanvasY;
      //--- Set initial canvas width from input
      m_currentWidth = initialCanvasWidth;
      //--- Set initial canvas height from input
      m_currentHeight = initialCanvasHeight;

      //--- Reset drag state
      m_isDragging = false;
      //--- Reset resize state
      m_isResizing = false;
      //--- Reset drag origin X
      m_dragStartX = 0;
      //--- Reset drag origin Y
      m_dragStartY = 0;
      //--- Reset canvas position snapshot X
      m_canvasStartX = 0;
      //--- Reset canvas position snapshot Y
      m_canvasStartY = 0;
      //--- Reset resize origin X
      m_resizeStartX = 0;
      //--- Reset resize origin Y
      m_resizeStartY = 0;
      //--- Reset width snapshot for resize
      m_resizeInitialWidth = 0;
      //--- Reset height snapshot for resize
      m_resizeInitialHeight = 0;
      //--- Reset active resize direction
      m_activeResizeMode = NO_RESIZE;
      //--- Reset hover resize direction
      m_hoverResizeMode = NO_RESIZE;

      //--- Reset canvas hover flag
      m_isHoveringCanvas = false;
      //--- Reset header hover flag
      m_isHoveringHeader = false;
      //--- Reset resize zone hover flag
      m_isHoveringResizeZone = false;
      //--- Reset switch icon hover flag
      m_isHoveringSwitchIcon = false;
      //--- Reset last mouse X
      m_lastMouseX = 0;
      //--- Reset last mouse Y
      m_lastMouseY = 0;
      //--- Reset previous mouse button state
      m_previousMouseButtonState = 0;

      //--- Set view mode from input
      m_currentViewMode = viewMode;
      //--- Mark 3D objects as not yet created
      m_are3DObjectsCreated = false;

      //--- Set camera distance from input
      m_cameraDistance = initialCameraDistance;
      //--- Set camera elevation angle from input
      m_cameraAngleX = initialCameraAngleX;
      //--- Set camera azimuth angle from input
      m_cameraAngleY = initialCameraAngleY;
      //--- Reset 3D rotation drag origin X
      m_mouse3DStartX = -1;
      //--- Reset 3D rotation drag origin Y
      m_mouse3DStartY = -1;
      //--- Mark 3D rotation as inactive
      m_isRotating3D = false;

      //--- Reset data range minimum
      m_minDataValue = 0.0;
      //--- Reset data range maximum
      m_maxDataValue = 0.0;
      //--- Reset peak histogram frequency
      m_maxFrequency = 0.0;
      //--- Reset peak theoretical PMF value
      m_maxTheoreticalValue = 0.0;
      //--- Mark data as not yet loaded
      m_isDataLoaded = false;

      //--- Reset sample mean
      m_sampleMean = 0.0;
      //--- Reset standard deviation
      m_sampleStandardDeviation = 0.0;
      //--- Reset skewness
      m_sampleSkewness = 0.0;
      //--- Reset kurtosis
      m_sampleKurtosis = 0.0;
      //--- Reset 25th percentile
      m_percentile25 = 0.0;
      //--- Reset median
      m_percentile50 = 0.0;
      //--- Reset 75th percentile
      m_percentile75 = 0.0;
      //--- Reset 95% CI lower bound
      m_confidenceInterval95Lower = 0.0;
      //--- Reset 95% CI upper bound
      m_confidenceInterval95Upper = 0.0;
      //--- Reset 99% CI lower bound
      m_confidenceInterval99Lower = 0.0;
      //--- Reset 99% CI upper bound
      m_confidenceInterval99Upper = 0.0;
}

Wir definieren den Konstruktor für die Klasse DistributionVisualizer, um die Membervariablen bei der Instanziierung mit Standardwerten oder auf Eingabewerten basierenden Werten zu initialisieren. Wir legen den Namen der Zeichenfläche auf DistCanvas fest und weisen ihr anhand der Benutzereingaben Anfangspositionen und Abmessungen zu. Als Nächstes setzen wir die Interaktionsflags auf false, setzen die Koordinatentracker auf Null und stellen den Skalierungsmodus auf NO_RESIZE ein. Wir setzen die Hover-Zustände auf false, die Maus-Tracker auf Null, den Anzeigemodus auf den Wert aus viewMode und das 3D-Objekt-Flag auf false. Für die Kamera legen wir die Anfangsposition und die Winkel fest, setzen den Wert für die 3D-Maus auf -1 und das Rotationsflag auf false. Abschließend setzen wir die Minimal- und Maximalwerte der Daten sowie das Ladeflag auf geeignete Startwerte und setzen die statistischen Kennzahlen auf Null, um die Klasse für den Betrieb vorzubereiten. Nun müssen wir unsere Elemente bei der Deinitialisierung zerstören; das können wir im Destruktor der Klasse tun. In der Regel wird dabei genau wie beim Konstruktor der Klassenname verwendet, nur dass ihm ein Tilde-Präfix vorangestellt wird.

//+------------------------------------------------------------------+
//| Release all 3D scene objects on destruction                      |
//+------------------------------------------------------------------+
~DistributionVisualizer(void)
  {
   //--- Get total number of histogram bar boxes
   int count = ArraySize(m_histogramBars);
   //--- Loop over every bar and release its DirectX resources
   for(int i = 0; i < count; i++)
     {
      m_histogramBars[i].Shutdown();
     }
   //--- Release ground plane DirectX resources
   m_groundPlane.Shutdown();
   //--- Release X axis DirectX resources
   m_axisX.Shutdown();
   //--- Release Y axis DirectX resources
   m_axisY.Shutdown();
   //--- Release Z axis DirectX resources
   m_axisZ.Shutdown();
  }

Hier definieren wir den Destruktor für die Klasse DistributionVisualizer, um sicherzustellen, dass die Ressourcen bei der Zerstörung des Objekts ordnungsgemäß bereinigt werden. Wir speichern mit ArraySize die Anzahl der Histogrammbalken in m_histogramBars und durchlaufen diese dann in einer Schleife, um die Methode Shutdown aufzurufen und die zugehörigen DirectX-Ressourcen für die 3D-Balken freizugeben. Anschließend rufen wir Shutdown für m_groundPlane, m_axisX, m_axisY und m_axisZ auf, um die Objekte der Bodenebene und der Achsen freizugeben, wodurch Speicherlecks verhindert und ein sauberes Beenden gewährleistet wird. Es ist wichtig zu verstehen, dass der Destruktor automatisch aufgerufen wird; seine Definition ist optional, wird jedoch für eine ordnungsgemäße Freigabe der Ressourcen empfohlen.

Als Nächstes werden wir die Klassenmitgliedsfunktionen definieren. Man kann sie zwar innerhalb der Klasse deklarieren und dann mithilfe des Geltungsbereichsoperators (::) außerhalb der Klasse definieren, aber wir werden sie zur Vereinfachung öffentlich innerhalb der Klasse als Funktionen deklarieren. Beginnen wir mit der 3D-Visualisierung.

Erstellen der dreidimensionalen Histogrammbalken und der Kamera

Hier nimmt die dreidimensionale Kernszene Gestalt an. Wir definieren Funktionen, um jeden Histogrammbalken als DirectX-Box-Primitive zu erstellen und einzufärben, berechnen anhand der tatsächlichen Balkenhöhen einen automatisch angepassten Kameraabstand, der die gesamte Szene im Bildausschnitt erfasst, erstellen die Bodenebene, die den Balken visuell verankert, konstruieren die X-, Y- und Z-Achsenlinien zur räumlichen Orientierung, aktualisieren in jedem Frame die Weltposition der Kamera und die Beleuchtungsrichtung auf der Grundlage der aktuellen Winkel- und Entfernungswerte und positionieren sowie skalieren jeden Balken dynamisch neu, sobald sich die zugrunde liegenden Verteilungsdaten ändern.

//+------------------------------------------------------------------+
//| Allocate and configure one DXBox per histogram bin               |
//+------------------------------------------------------------------+
bool create3DHistogramBars()
  {
   //--- Allocate the bar array to match the required bin count
   ArrayResize(m_histogramBars, histogramCells);

   //--- Decompose histogram colour into RGB byte components
   uchar r = (uchar)((histogramColor)       & 0xFF);
   uchar g = (uchar)((histogramColor >> 8)  & 0xFF);
   uchar b = (uchar)((histogramColor >> 16) & 0xFF);

   //--- Create and configure each bar box
   for(int i = 0; i < histogramCells; i++)
     {
      //--- Create unit box; actual transform applied later in update step
      if(!m_histogramBars[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                    DXVector3(-0.5f, 0.0f, -0.5f),
                                    DXVector3(0.5f, 1.0f, 0.5f)))
        {
         Print("ERROR: Failed to create 3D box for bar ", i);
         return false;
        }

      //--- Set the bar's base diffuse colour from the histogram colour input
      m_histogramBars[i].DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f, 1.0f));
      //--- Add a subtle specular highlight for depth perception
      m_histogramBars[i].SpecularColorSet(DXColor(0.2f, 0.2f, 0.2f, 0.3f));
      //--- Set specular shininess exponent
      m_histogramBars[i].SpecularPowerSet(32.0f);
      //--- Disable self-emission (bars lit only by scene lights)
      m_histogramBars[i].EmissionColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));

      //--- Register the bar with the 3D scene
      m_mainCanvas.ObjectAdd(GetPointer(m_histogramBars[i]));
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Compute an optimal camera distance and angles for the scene      |
//+------------------------------------------------------------------+
void autoFitCameraPosition()
  {
   //--- Abort if not in 3D mode or data has not been loaded
   if(m_currentViewMode != VIEW_3D_MODE || !m_isDataLoaded) return;

   //--- Fixed scene width used for distance estimation
   float totalWidth  = 30.0f;
   //--- Track the tallest bar in the scene
   float maxBarHeight = 0.0f;

   //--- Use the peak theoretical value as the Y scale reference
   double rangeY = m_maxTheoreticalValue;
   //--- Guard against division by zero
   if(rangeY == 0) rangeY = 1;

   //--- Find the maximum normalised bar height across all bins
   for(int i = 0; i < histogramCells; i++)
     {
      float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
      float barHeight = normalizedHeight * 15.0f;
      if(barHeight > maxBarHeight) maxBarHeight = barHeight;
     }

   //--- Determine bounding extents for the entire scene
   float sceneWidth  = totalWidth;
   float sceneHeight = MathMax(maxBarHeight, 15.0f);
   float sceneDepth  = 10.0f;

   //--- Compute the scene bounding diagonal for FOV-based fitting
   float diagonal = MathSqrt(sceneWidth  * sceneWidth  +
                             sceneHeight * sceneHeight +
                             sceneDepth  * sceneDepth);

   //--- Match the projection FOV used in initialize3DContext
   float fov = (float)(DX_PI / 6.0);
   //--- Derive camera distance so the scene fills the view with a 1.5x margin
   m_cameraDistance = (diagonal / 2.0f) / MathTan(fov / 2.0f) * 1.5;

   //--- Set a comfortable default elevation angle
   m_cameraAngleX = 0.5;
   //--- Set a comfortable default azimuth angle
   m_cameraAngleY = 0.7;

   //--- Enforce minimum distance to avoid clipping near-plane
   if(m_cameraDistance <  35.0) m_cameraDistance =  35.0;
   //--- Enforce maximum distance to keep bars visible
   if(m_cameraDistance > 100.0) m_cameraDistance = 100.0;

   Print("Auto-fit camera: Distance = ", m_cameraDistance,
         ", AngleX = ", m_cameraAngleX, ", AngleY = ", m_cameraAngleY);
  }

//+------------------------------------------------------------------+
//| Create the flat ground reference plane in 3D space               |
//+------------------------------------------------------------------+
bool createGroundPlane()
  {
   //--- Build a thin box spanning the configured width and depth
   if(!m_groundPlane.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                            DXVector3(-groundPlaneWidth / 2.0f, -groundPlaneThickness, -groundPlaneDepth / 2.0f),
                            DXVector3( groundPlaneWidth / 2.0f,  0.0f,                  groundPlaneDepth / 2.0f)))
     {
      Print("ERROR: Failed to create ground plane");
      return false;
     }

   //--- Decompose ground colour into RGB byte components (BGR layout)
   uchar r = (uchar)((groundPlaneColor >> 16) & 0xFF);
   uchar g = (uchar)((groundPlaneColor >> 8)  & 0xFF);
   uchar b = (uchar)( groundPlaneColor         & 0xFF);
   //--- Apply the configured ground colour and opacity
   m_groundPlane.DiffuseColorSet(DXColor(r / 255.0f, g / 255.0f, b / 255.0f,
                                         (float)groundPlaneOpacity));
   //--- Remove specular highlight for a flat matte look
   m_groundPlane.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));

   //--- Register the ground plane with the 3D scene
   m_mainCanvas.ObjectAdd(GetPointer(m_groundPlane));

   return true;
  }

//+------------------------------------------------------------------+
//| Create colour-coded X, Y, and Z coordinate axis boxes            |
//+------------------------------------------------------------------+
bool create3DAxes()
  {
   //--- Create X axis as a thin horizontal box along positive X
   if(!m_axisX.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisLength, axisThickness, axisThickness)))
     {
      Print("ERROR: Failed to create X axis");
      return false;
     }
   //--- Extract X axis colour components
   uchar rx = (uchar)((axisXColor >> 16) & 0xFF);
   uchar gx = (uchar)((axisXColor >> 8)  & 0xFF);
   uchar bx = (uchar)( axisXColor         & 0xFF);
   //--- Apply X axis diffuse colour
   m_axisX.DiffuseColorSet(DXColor(rx / 255.0f, gx / 255.0f, bx / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisX.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register X axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisX));

   //--- Create Y axis as a thin vertical box along positive Y
   if(!m_axisY.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisThickness, axisLength, axisThickness)))
     {
      Print("ERROR: Failed to create Y axis");
      return false;
     }
   //--- Extract Y axis colour components
   uchar ry = (uchar)((axisYColor >> 16) & 0xFF);
   uchar gy = (uchar)((axisYColor >> 8)  & 0xFF);
   uchar by = (uchar)( axisYColor         & 0xFF);
   //--- Apply Y axis diffuse colour
   m_axisY.DiffuseColorSet(DXColor(ry / 255.0f, gy / 255.0f, by / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisY.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register Y axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisY));

   //--- Create Z axis as a thin depth box along positive Z
   if(!m_axisZ.Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                      DXVector3(0.0f, 0.0f, 0.0f),
                      DXVector3(axisThickness, axisThickness, axisLength)))
     {
      Print("ERROR: Failed to create Z axis");
      return false;
     }
   //--- Extract Z axis colour components
   uchar rz = (uchar)((axisZColor >> 16) & 0xFF);
   uchar gz = (uchar)((axisZColor >> 8)  & 0xFF);
   uchar bz = (uchar)( axisZColor         & 0xFF);
   //--- Apply Z axis diffuse colour
   m_axisZ.DiffuseColorSet(DXColor(rz / 255.0f, gz / 255.0f, bz / 255.0f, 1.0f));
   //--- Remove specular for clean axis appearance
   m_axisZ.SpecularColorSet(DXColor(0.0f, 0.0f, 0.0f, 0.0f));
   //--- Register Z axis with the scene
   m_mainCanvas.ObjectAdd(GetPointer(m_axisZ));

   return true;
  }

//+------------------------------------------------------------------+
//| Recompute and apply the view matrix from spherical coordinates   |
//+------------------------------------------------------------------+
void updateCameraPosition()
  {
   //--- Only apply in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Start with a camera positioned along the negative Z axis
   DXVector4 camera = DXVector4(0.0f, 0.0f, (float)(-m_cameraDistance), 1.0f);

   //--- Rotate camera around the X axis by the elevation angle
   DXMatrix rotationX;
   DXMatrixRotationX(rotationX, (float)m_cameraAngleX);
   DXVec4Transform(camera, camera, rotationX);

   //--- Rotate the result around the Y axis by the azimuth angle
   DXMatrix rotationY;
   DXMatrixRotationY(rotationY, (float)m_cameraAngleY);
   DXVec4Transform(camera, camera, rotationY);

   //--- Apply the final camera world position
   m_mainCanvas.ViewPositionSet(DXVector3(camera));

   //--- Place the key light slightly above the camera position
   DXVector3 cameraPos = DXVector3(camera.x, camera.y, camera.z);
   DXVector3 lightPos  = DXVector3(cameraPos.x, cameraPos.y + 10.0f, cameraPos.z);
   //--- Scene origin is always the light target
   DXVector3 target    = DXVector3(0.0f, 0.0f, 0.0f);

   //--- Compute the raw light direction vector
   DXVector3 lightDir;
   lightDir.x = target.x - lightPos.x;
   lightDir.y = target.y - lightPos.y;
   lightDir.z = target.z - lightPos.z;

   //--- Compute the vector length for normalisation
   float length = MathSqrt(lightDir.x * lightDir.x +
                           lightDir.y * lightDir.y +
                           lightDir.z * lightDir.z);
   //--- Normalise the direction vector if it has non-zero length
   if(length > 0.0f)
     {
      lightDir.x /= length;
      lightDir.y /= length;
      lightDir.z /= length;
     }

   //--- Apply the normalised light direction to the scene
   m_mainCanvas.LightDirectionSet(lightDir);
  }

//+------------------------------------------------------------------+
//| Reposition and scale every 3D histogram bar to match data        |
//+------------------------------------------------------------------+
void update3DHistogramBars()
  {
   //--- Abort if data has not been loaded
   if(!m_isDataLoaded) return;

   //--- Compute the X data range for spatial mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   //--- Use the peak PMF value as the height scale reference
   double rangeY = m_maxTheoreticalValue;
   //--- Guard against division by zero for X
   if(rangeX == 0) rangeX = 1;
   //--- Guard against division by zero for Y
   if(rangeY == 0) rangeY = 1;

   //--- Total scene width used to space the bars evenly
   float totalWidth = 30.0f;
   //--- Compute even spacing for each bar slot
   float barSpacing = totalWidth / (float)histogramCells;
   //--- Make each bar 80% of its slot to leave a small gap
   float barWidth   = barSpacing * 0.8f;
   //--- Shift origin so bars are centred on the scene
   float offsetX    = -totalWidth / 2.0f;

   //--- Update scale and position of every bar
   for(int i = 0; i < histogramCells; i++)
     {
      //--- Normalise this bin's frequency against the peak PMF
      float normalizedHeight = (float)(m_histogramFrequencies[i] / rangeY);
      //--- Map to scene height units (max 15 units tall)
      float barHeight = normalizedHeight * 15.0f;
      //--- Enforce a minimum visible height so bars are always rendered
      if(barHeight < 0.5f) barHeight = 0.5f;

      //--- Compute the bar's X centre in scene space
      float xPos = offsetX + (float)i * barSpacing + barWidth / 2.0f;

      //--- Build scale, translation and combined transform matrices
      DXMatrix scale, translation, transform;
      DXMatrixScaling(scale, barWidth, barHeight, barWidth);
      DXMatrixTranslation(translation, xPos, 0.0f, 0.0f);
      DXMatrixMultiply(transform, scale, translation);

      //--- Apply the combined world transform to this bar
      m_histogramBars[i].TransformMatrixSet(transform);
     }
  }

Zunächst definieren wir die Funktion create3DHistogramBars, um die 3D-Balken für das Histogramm zu initialisieren. Wir passen die Größe des Arrays m_histogramBars mit ArrayResize an histogramCells an, extrahieren die RGB-Komponenten mithilfe von Bitoperationen aus histogramColor und durchlaufen anschließend die Zellen, um jede CDXBox mit der Methode Create zu erstellen, wobei der DX-Dispatcher und die Eingabeszene sowie die Vektorabmessungen für eine Einheitsbox übergeben werden. Wenn die Erstellung fehlschlägt, geben wir eine Fehlermeldung aus und geben false zurück; ansonsten wird die diffuse Farbe über DiffuseColorSet unter Verwendung von normalisiertem RGB festgelegt, die Spiegelung mit SpecularColorSet und die Stärke über SpecularPowerSet angewendet, die Emission mit EmissionColorSet auf Null gesetzt und die Box mithilfe von ObjectAdd mit einem Zeiger zur Zeichenfläche hinzugefügt, wobei bei Erfolg true zurückgegeben wird.

Um die Kamera automatisch für eine optimale Ansicht zu positionieren, implementieren wir die Funktion autoFitCameraPosition, die vorzeitig beendet wird, wenn der Modus VIEW_3D_MODE nicht aktiv ist oder wenn die Daten noch nicht geladen wurden. Wir legen eine Gesamtbreite fest, ermitteln die maximale normalisierte Balkenhöhe, skaliert auf 15,0f, indem wir eine Schleife über die Frequenzen, geteilt durch den Y-Bereich, durchlaufen, berechnen die Szenenabmessungen mit MathMax für die Höhe, leiten die Diagonale mit MathSqrt ab und berechnen m_cameraDistance basierend auf dem Sichtfeld mit MathTan unter Anwendung eines Multiplikators von 1,5. Wir weisen m_cameraAngleX und m_cameraAngleY feste Winkelwerte zu, begrenzen den Abstand auf einen Bereich zwischen 35,0 und 100,0 und geben die Einstellungen zur Fehlersuche aus.

Als Nächstes erstellen wir die Funktion createGroundPlane, um eine Bodenebene im 3D-Raum hinzuzufügen. Wir rufen die Methode Create für m_groundPlane mit Vektoren auf, deren Mittelpunkte im Ursprung liegen und die an die eingegebenen Abmessungen und die Dicke angepasst sind; bei einem Fehler geben wir eine Fehlermeldung aus und kehren den Wert false zurück. Anschließend extrahieren wir die RGB-Werte aus groundPlaneColor, setzen die diffuse Beleuchtung mit DiffuseColorSet, wobei groundPlaneOpacity für die Transparenz berücksichtigt wird, die spiegelnde Beleuchtung auf Null und fügen sie über ObjectAdd zur Zeichenfläche hinzu, wobei true zurückgegeben wird. Zur Orientierung definieren wir die Funktion create3DAxes, um die X-, Y- und Z-Achsen zu erstellen, wenn show3DAxes wahr ist. Für jede Achse rufen wir Create mit den entsprechenden Vektorgrößen entlang ihrer Richtungen auf, extrahieren RGB aus den jeweiligen Farben wie axisXColor, legen mit DiffuseColorSet und SpecularColorSet die diffuse Farbe als vollständig undurchsichtig und die Spiegelungsfarbe als deaktiviert fest und fügen sie mithilfe von ObjectAdd zur Zeichenfläche hinzu und geben true zurück, wenn alles erfolgreich war, oder false bei Fehlern.

Anschließend implementieren wir die Funktion updateCameraPosition, um die 3D-Ansicht anzupassen, und kehren zurück, wenn sich das Programm nicht im 3D-Modus befindet. Wir bilden einen Kameravektor bei negativem m_cameraDistance auf der Z-Achse, erstellen Rotationsmatrizen mit DXMatrixRotationX und DXMatrixRotationY auf Basis der Winkel, transformieren den Vektor nacheinander mit DXVec4Transform und legen die Blickposition über ViewPositionSet fest. Um gerichtetes Licht zu simulieren, leiten wir die Lichtposition über der Kamera ab, berechnen und normalisieren die Richtung zum Ziel am Ursprung mit MathSqrt und wenden sie mit LightDirectionSet an.

Abschließend definieren wir die Funktion update3DHistogramBars, um die Balken dynamisch zu positionieren und zu skalieren; die Funktion wird beendet, wenn keine Daten vorliegen. Nach der Berechnung der Bereiche mit Sicherheitsabständen legen wir die Gesamtbreite auf 30,0f fest, leiten den Abstand und die Balkenbreite sowie den Versatz für die Zentrierung ab, führen dann in einer Schleife eine Normalisierung der auf 15,0f skalierten Höhen mit einem Mindestwert von 0,5f durch, berechnen die X-Positionen, erstellen eine Skalierungsmatrix mit DXMatrixScaling und eine Translationsmatrix mit DXMatrixTranslation, multiplizieren diese mit DXMatrixMultiply und wenden die Transformation mit TransformMatrixSet auf jeden Balken an, um die 3D-Platzierung zu gewährleisten. Als Nächstes werden wir die Kopfzeile und den Rahmen für die 3D-Darstellung zeichnen.

Zeichnen der Kopfzeile, des Umschaltsymbols und des Rahmens im dreidimensionalen Modus

Selbst im dreidimensionalen Modus benötigt das Tool eine ordentliche Kopfleiste zur Anzeige des Titels der Verteilung, eine deutlich positionierte Umschaltfläche, mit der der Benutzer mit einem einzigen Klick in den zweidimensionalen Modus zurückwechseln kann, sowie einen Rahmen, der die gesamte Zeichenfläche umgibt – alles als zweidimensionale Überlagerungen über der DirectX-Szene dargestellt, damit die Benutzeroberfläche unabhängig vom aktiven Ansichtsmodus vertraut und funktionsfähig bleibt.

//+------------------------------------------------------------------+
//| Draw the circular 2D/3D toggle icon in the header bar            |
//+------------------------------------------------------------------+
void drawSwitchIcon()
  {
   //--- Compute the icon's top-left corner from the canvas right margin
   int iconX = m_currentWidth  - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
   //--- Vertically centre the icon within the header bar
   int iconY = (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;

   //--- Compute icon background colour from hover state
   color iconBgColor = m_isHoveringSwitchIcon
                       ? DarkenColor(themeColor, 0.1)     // Slightly darker on hover
                       : LightenColor(themeColor, 0.5);   // Default lighter shade

   uint argbIconBg = ColorToARGB(iconBgColor, 255);

   //--- Fill the circular icon background
   m_mainCanvas.FillCircle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                           SWITCH_ICON_SIZE / 2, argbIconBg);

   //--- Draw the circular icon border using the theme colour
   uint argbBorder = ColorToARGB(themeColor, 255);
   m_mainCanvas.Circle(iconX + SWITCH_ICON_SIZE / 2, iconY + SWITCH_ICON_SIZE / 2,
                       SWITCH_ICON_SIZE / 2, argbBorder);

   //--- Set a small bold font for the mode label
   m_mainCanvas.FontSet("Arial Bold", 10);
   uint argbLabel = ColorToARGB(clrWhite, 255);

   //--- Display "2D" or "3D" depending on the active mode
   string modeLabel = (m_currentViewMode == VIEW_2D_MODE) ? "2D" : "3D";
   m_mainCanvas.TextOut(iconX + SWITCH_ICON_SIZE / 2, iconY + (SWITCH_ICON_SIZE - 10) / 2,
                        modeLabel, argbLabel, TA_CENTER);
  }

//+------------------------------------------------------------------+
//| Draw the header bar overlaid on the 3D rendered scene            |
//+------------------------------------------------------------------+
void drawHeaderBarOn3D()
  {
   //--- Compute the header fill colour from the current interaction state
   color headerColor;
   if(m_isDragging)
      headerColor = DarkenColor(themeColor, 0.1);       // Slightly darker while dragging
   else if(m_isHoveringHeader)
      headerColor = LightenColor(themeColor, 0.4);      // Medium light on hover
   else
      headerColor = LightenColor(themeColor, 0.7);      // Very light at rest

   uint argbHeader = ColorToARGB(headerColor, 255);

   //--- Paint over the top portion of the 3D render with the header colour
   m_mainCanvas.FillRectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbHeader);

   //--- Overlay a border frame on the header if enabled
   if(showBorderFrame)
     {
      uint argbBorder = ColorToARGB(themeColor, 255);
      m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, HEADER_BAR_HEIGHT, argbBorder);
      m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, HEADER_BAR_HEIGHT - 1, argbBorder);
     }

   //--- Set the bold title font
   m_mainCanvas.FontSet("Arial Bold", titleFontSize);
   uint argbText = ColorToARGB(titleTextColor, 255);

   //--- Format the title string with current parameters and 3D label
   string titleText = StringFormat("Binomial Distribution (n=%d, p=%.2f) - 3D View",
                                   numTrials, successProbability);
   //--- Draw the title centred horizontally within the header
   m_mainCanvas.TextOut(m_currentWidth / 2, (HEADER_BAR_HEIGHT - titleFontSize) / 2,
                        titleText, argbText, TA_CENTER);

   //--- Draw the interactive 2D/3D mode switch icon
   drawSwitchIcon();
  }
//+------------------------------------------------------------------+
//| Draw the outer and inner border rectangles for 3D overlay        |
//+------------------------------------------------------------------+
void draw3DBorder()
  {
   //--- Use a slightly darker border when hovering a resize zone
   color borderColor = m_isHoveringResizeZone ? DarkenColor(themeColor, 0.2) : themeColor;
   uint  argbBorder  = ColorToARGB(borderColor, 255);

   //--- Draw the outermost border rectangle over the 3D render
   m_mainCanvas.Rectangle(0, 0, m_currentWidth - 1, m_currentHeight - 1, argbBorder);
   //--- Draw an inner border rectangle for a double-line effect
   m_mainCanvas.Rectangle(1, 1, m_currentWidth - 2, m_currentHeight - 2, argbBorder);
  }

Hier definieren wir die Funktion drawSwitchIcon, um in der Kopfzeile eine Umschalt-Schaltfläche zum Wechseln zwischen den Ansichten darzustellen. Wir berechnen die Position des Symbols anhand von Konstanten wie SWITCH_ICON_SIZE und SWITCH_ICON_MARGIN, wählen eine Hintergrundfarbe aus, die beim Überfahren mit der Maus mit DarkenColor abgedunkelt oder andernfalls mit LightenColor aufgehellt wird, konvertieren sie mit ColorToARGB in ARGB und füllen einen Kreis mit der Methode FillCircle. Wir fügen mit Circle einen Umrandungskreis hinzu, legen mit FontSet eine fette Schriftart fest, bereiten weißen ARGB-Text vor, formatieren eine Beschriftung je nach m_currentViewMode als 2D oder 3D und zentrieren sie mit TextOut, um interaktives Feedback zu erhalten.

Als Nächstes erstellen wir die Funktion drawHeaderBarOn3D, um im 3D-Modus eine Kopfzeile einzublenden – ähnlich wie im 2D-Modus, jedoch mit einem geänderten Titel. Wir legen die Farbe des Headers je nach Drag- oder Hover-Zustand mithilfe von DarkenColor oder LightenColor fest, füllen das Rechteck mit FillRectangle, fügen über Rectangle Rahmen hinzu, wenn showBorderFrame auf true gesetzt ist, legen die Schriftart fest, formatieren einen Titel einschließlich - 3D-Ansicht mit StringFormat, zeichnen ihn zentriert mit TextOut und rufen drawSwitchIcon auf, um den Umschalter einzubinden. Um die Zeichenfläche in 3D einzurahmen, implementieren wir die Funktion draw3DBorder, wählen eine Rahmenfarbe aus, die beim Überfahren mit der Maus und bei einer Größenänderung mit DarkenColor abgedunkelt wird, konvertieren diese in ARGB und zeichnen mit Rectangle innere und äußere Rechtecke, um die Konsistenz mit 2D-Rahmen zu gewährleisten. Als Nächstes zeichnen wir die theoretische 3D-Kurve so, dass sie der Einheitlichkeit halber die 3D-Ebene nutzt. Der Einfachheit halber verwenden wir vorerst eine normale Linienkurve.

Projektion der theoretischen Kurve auf die dreidimensionale Szene

Während die Histogrammbalken in der DirectX-Szene als echte dreidimensionale Objekte vorliegen, wird die theoretische PMF als zweidimensionale Überlagerung gezeichnet, die in den perspektivischen Raum projiziert wird – das bedeutet, dass wir jeden Kurvenpunkt manuell mithilfe der kombinierten Ansichts- und Projektionsmatrizen transformieren, um zu berechnen, wo er auf dem Bildschirm landet, und ihn dann als anti-aliaste Linie auf der Zeichenfläche zeichnen. Außerdem haben wir eine Beschneidungsfunktion integriert, um zu verhindern, dass irgendein Teil der Kurve über die Kopfzeile hinausgerendert wird.

//+------------------------------------------------------------------+
//| Project the theoretical PMF curve into 3D screen space           |
//+------------------------------------------------------------------+
void draw3DTheoreticalCurve()
  {
   //--- Abort if data has not been loaded yet
   if(!m_isDataLoaded) return;

   //--- Compute the data ranges for world-space mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   double rangeY = m_maxTheoreticalValue;
   if(rangeX == 0) rangeX = 1;
   if(rangeY == 0) rangeY = 1;

   //--- Match the total scene width used for the histogram bars
   float totalWidth = 30.0f;
   float offsetX    = -totalWidth / 2.0f;

   //--- Retrieve the current view-projection matrices for projection
   DXMatrix projection, view, worldToScreen;
   m_mainCanvas.ViewMatrixGet(view);
   m_mainCanvas.ProjectionMatrixGet(projection);
   //--- Combine view and projection into a single world-to-clip matrix
   DXMatrixMultiply(worldToScreen, view, projection);

   uint curveColor = ColorToARGB(theoreticalCurveColor, 255);

   //--- Transform and draw each consecutive pair of PMF samples
   for(int i = 0; i < ArraySize(m_theoreticalXValues) - 1; i++)
     {
      //--- Map PMF X and Y values into 3D world space
      float x1 = offsetX + (float)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * totalWidth);
      float y1 = (float)(m_theoreticalYValues[i]     / rangeY * 20.0);
      float x2 = offsetX + (float)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * totalWidth);
      float y2 = (float)(m_theoreticalYValues[i + 1] / rangeY * 20.0);

      //--- Build homogeneous 3D points on the Z = 0 plane
      DXVector4 p1_3d = DXVector4(x1, y1, 0.0f, 1.0f);
      DXVector4 p2_3d = DXVector4(x2, y2, 0.0f, 1.0f);

      //--- Project both points into clip space
      DXVec4Transform(p1_3d, p1_3d, worldToScreen);
      DXVec4Transform(p2_3d, p2_3d, worldToScreen);

      //--- Perform perspective divide only for points in front of the near plane
      if(p1_3d.w > 0.0f && p2_3d.w > 0.0f)
        {
         DXVec4Scale(p1_3d, p1_3d, 1.0f / p1_3d.w);
         DXVec4Scale(p2_3d, p2_3d, 1.0f / p2_3d.w);

         //--- Convert NDC coordinates to pixel coordinates
         int sx1 = (int)((float)m_currentWidth  * (0.5f + 0.5f * p1_3d.x));
         int sy1 = (int)((float)m_currentHeight * (0.5f - 0.5f * p1_3d.y));
         int sx2 = (int)((float)m_currentWidth  * (0.5f + 0.5f * p2_3d.x));
         int sy2 = (int)((float)m_currentHeight * (0.5f - 0.5f * p2_3d.y));

         //--- Clip line endpoints against the header bar boundary
         if(clipLineToHeader(sx1, sy1, sx2, sy2))
           {
            //--- Draw multiple offset passes for the configured line width
            for(int w = 0; w < curveLineWidth; w++)
               m_mainCanvas.LineAA(sx1, sy1 + w, sx2, sy2 + w, curveColor);
           }
        }
     }
  }

//+------------------------------------------------------------------+
//| Clip a line segment to exclude the header bar region             |
//+------------------------------------------------------------------+
bool clipLineToHeader(int &x1, int &y1, int &x2, int &y2)
  {
   //--- Reject the segment entirely if both endpoints are inside the header
   if(y1 < HEADER_BAR_HEIGHT && y2 < HEADER_BAR_HEIGHT)
      return false;

   //--- Accept the segment entirely if both endpoints are below the header
   if(y1 >= HEADER_BAR_HEIGHT && y2 >= HEADER_BAR_HEIGHT)
      return true;

   //--- Clip the first endpoint when it falls inside the header
   if(y1 < HEADER_BAR_HEIGHT)
     {
      if(y2 != y1)
        {
         //--- Linearly interpolate X to find the intersection with the header boundary
         x1 = x1 + (x2 - x1) * (HEADER_BAR_HEIGHT - y1) / (y2 - y1);
         y1 = HEADER_BAR_HEIGHT;
        }
     }
   //--- Clip the second endpoint when it falls inside the header
   else if(y2 < HEADER_BAR_HEIGHT)
     {
      if(y2 != y1)
        {
         //--- Linearly interpolate X to find the intersection with the header boundary
         x2 = x1 + (x2 - x1) * (HEADER_BAR_HEIGHT - y1) / (y2 - y1);
         y2 = HEADER_BAR_HEIGHT;
        }
     }

   return true;
  }

Hier definieren wir die Funktion draw3DTheoreticalCurve, um die theoretische Wahrscheinlichkeitsmassfunktion als 2D-Linie über die 3D-Szene zu legen und so sicherzustellen, dass sie perspektivisch korrekt dargestellt wird, ohne dass eine vollständige 3D-Kurvenmodellierung erforderlich ist. Das wäre zwar möglich, aber wir möchten das vorerst nicht tun, da wir uns ausschließlich auf die 3D-Balken konzentrieren wollen. Wir kehren vorzeitig zurück, wenn die Daten nicht geladen wurden, berechnen die X- und Y-Bereiche mit Sicherheitsabständen, legen eine Gesamtbreite fest, die dem Histogramm zur Ausrichtung entspricht, und berechnen einen Versatz zur Zentrierung.

Um 3D-Punkte auf den 2D-Bildschirmraum zu projizieren, deklarieren wir Matrizen, rufen die Ansichts- und Projektionsmatrix mit ViewMatrixGet und ProjectionMatrixGet ab, multiplizieren diese mithilfe von DXMatrixMultiply zu einer Welt-zu-Bildschirm-Matrix und bereiten die Kurvenfarbe über ColorToARGB vor. In einer Schleife über aufeinanderfolgende theoretische Punkte skalieren wir die X- und Y-Koordinaten so, dass sie in den 3D-Raum passen, bilden DXVector4-Punkte bei z=0, transformieren diese mit DXVec4Transform, prüfen anhand eines positiven w-Werts die Sichtbarkeit, normalisieren durch Division mit DXVec4Scale, wandeln sie basierend auf den Canvas-Abmessungen in Bildschirm-Ganzzahlen um, beschneiden das Segment mit clipLineToHeader, um den Kopfbereich zu vermeiden, und zeichnen mit LineAA anti-aliaste Linien in einer Schleife über die Breite curveLineWidth für die Strichstärke.

Diese Projektionstechnik ist von entscheidender Bedeutung, da sie eine Brücke zwischen 3D-Rendering und 2D-Überlagerungen schlägt: Durch die Transformation der Weltkoordinaten mittels der kombinierten Matrix simulieren wir Tiefe, während wir flache Linien auf die Zeichenfläche zeichnen. Dadurch wirkt die Kurve so, als würde sie relativ zu den Balken im 3D-Raum schweben, was den visuellen Zusammenhang ohne komplexe 3D-Spline-Interpolation verstärkt. Um zu verhindern, dass die Zeichnung über den Kopfbereich hinausragt, implementieren wir die Funktion clipLineToHeader als einfaches Hilfsmittel zum Abschneiden von Linien an der Kopfbereichsgrenze. Wir lehnen den Punkt ab, wenn beide y-Koordinaten über HEADER_BAR_HEIGHT liegen, akzeptieren ihn, wenn beide darunter liegen, und schneiden andernfalls den betreffenden Punkt ab, indem wir x am y-Grenzwert interpolieren und die Koordinaten entsprechend anpassen, um eine saubere Integration der 2D-Elemente in die 3D-Ansicht zu gewährleisten. Wir können nun mit der Initialisierung der 3D-Darstellung fortfahren, damit wir unseren Fortschritt sehen können. Wir werden nun die Logik zur Erstellung der Visualisierung definieren.

Erstellen und Initialisieren des dreidimensionalen Kontexts

Nachdem nun alle einzelnen Zeichen- und Kamerafunktionen definiert sind, benötigen wir nun die Logik, die alles miteinander verknüpft – das Erstellen der Zeichenfläche, das Konfigurieren des DirectX-Kontexts, das Zusammenstellen der dreidimensionalen Objekte, das Laden der Verteilungsdaten und das Auslösen des ersten Rendervorgangs. Diese Funktionen bilden das Rückgrat der Startsequenz des Tools. Jede davon hat eine klar definierte Aufgabe: Die Erstellung der Zeichenfläche richtet die Rendering-Oberfläche ein, die Initialisierung des Kontexts konfiguriert Beleuchtung und Projektion, die Objekterstellung füllt die Szene mit Objekten, und das Laden der Daten versorgt das Histogramm mit simulierten binomialen Stichproben. Ohne diese Orchestrierungsschicht würde keine der zuvor definierten einzelnen Funktionen in der richtigen Reihenfolge ausgelöst werden.

//+------------------------------------------------------------------+
//| Dispatch rendering to the active 2D or 3D pipeline               |
//+------------------------------------------------------------------+
void renderVisualization()
  {
   //--- Render using the 2D canvas pipeline
   if(m_currentViewMode == VIEW_2D_MODE)
      render2DVisualization();
   else
      //--- Render using the 3D DirectX pipeline
      render3DVisualization();
  }
//+------------------------------------------------------------------+
//| Render the 3D scene and overlay 2D UI elements on top            |
//+------------------------------------------------------------------+
void render3DVisualization()
  {
   //--- Abort if data has not been loaded
   if(!m_isDataLoaded) return;

   //--- Recompute the view and light transforms for this frame
   updateCameraPosition();
   //--- Reposition all 3D histogram bars to match current data
   update3DHistogramBars();

   //--- Use the configured background colour for the clear pass
   color bgColor     = backgroundTopColor;
   uint  bgColorArgb = ColorToARGB(bgColor, 255);
   //--- Clear colour and depth buffers, then render the 3D scene
   m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

   //--- Overlay the 2D border frame on top of the 3D render
   if(showBorderFrame)
      draw3DBorder();

   //--- Overlay the header bar on the rendered 3D image
   drawHeaderBarOn3D();
   //--- Overlay the stats panel and legend if enabled
   if(showStatistics)
     {
      drawStatisticsPanelOn3D();
      drawLegendOn3D();
     }
   //--- Project and draw the theoretical PMF curve into screen space
   draw3DTheoreticalCurve();

   //--- Draw the resize grip indicator when hovering
   if(m_isHoveringResizeZone && enableResizing)
      drawResizeIndicatorOn3D();

   //--- Flush the pixel buffer to the chart object
   m_mainCanvas.Update();
  }
//+------------------------------------------------------------------+
//| Create bitmap label, initialise 3D context and scene objects     |
//+------------------------------------------------------------------+
bool createCanvasAndObjects()
  {
   //--- Create the canvas bitmap label on the chart
   if(!m_mainCanvas.CreateBitmapLabel(m_canvasObjectName, 0, 0, m_currentWidth, m_currentHeight,
                                      COLOR_FORMAT_ARGB_NORMALIZE))
     {
      Print("ERROR: Failed to create canvas");
      return false;
     }

   //--- Position the canvas horizontally
   ObjectSetInteger(0, m_canvasObjectName, OBJPROP_XDISTANCE, m_currentPositionX);
   //--- Position the canvas vertically
   ObjectSetInteger(0, m_canvasObjectName, OBJPROP_YDISTANCE, m_currentPositionY);

   //--- Initialise the DirectX 3D rendering context
   if(!initialize3DContext())
     {
      Print("ERROR: Failed to initialize 3D context");
      return false;
     }

   //--- Build all 3D scene objects (bars, ground, axes)
   if(!create3DObjects())
     {
      Print("ERROR: Failed to create 3D objects");
      return false;
     }

   return true;
  }

//+------------------------------------------------------------------+
//| Set up projection, lighting and initial camera for 3D rendering  |
//+------------------------------------------------------------------+
bool initialize3DContext()
  {
   //--- Set perspective projection matrix with 30-degree FOV
   m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                    (float)m_currentWidth / (float)m_currentHeight,
                                    0.1f, 1000.0f);

   //--- Point the camera at the scene origin
   m_mainCanvas.ViewTargetSet(DXVector3(0.0f, 0.0f, 0.0f));
   //--- Define world up direction as positive Y
   m_mainCanvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f));

   //--- Set directional light colour to near-white
   m_mainCanvas.LightColorSet(DXColor(1.0f, 1.0f, 1.0f, 0.9f));
   //--- Set ambient light colour for soft fill
   m_mainCanvas.AmbientColorSet(DXColor(0.6f, 0.6f, 0.6f, 0.5f));

   //--- Auto-fit camera if enabled and data is ready
   if(autoFitCamera && m_isDataLoaded)
      autoFitCameraPosition();

   //--- Recompute and apply the camera transform
   updateCameraPosition();

   Print("SUCCESS: 3D context initialized");
   return true;
  }
//+------------------------------------------------------------------+
//| Build all 3D scene objects: bars, ground plane, and axes         |
//+------------------------------------------------------------------+
bool create3DObjects()
  {
   //--- Create the histogram bar boxes
   if(!create3DHistogramBars())
     {
      Print("ERROR: Failed to create 3D histogram bars");
      return false;
     }

   //--- Create the flat ground reference plane
   if(!createGroundPlane())
     {
      Print("ERROR: Failed to create ground plane");
      return false;
     }

   //--- Create coordinate axes only when the option is enabled
   if(show3DAxes && !create3DAxes())
     {
      Print("ERROR: Failed to create 3D axes");
      return false;
     }

   //--- Flag that all 3D objects are ready
   m_are3DObjectsCreated = true;
   Print("SUCCESS: 3D objects created");
   return true;
  }

//+------------------------------------------------------------------+
//| Prepare the scene for 3D rendering after a mode switch           |
//+------------------------------------------------------------------+
bool setup3DMode()
  {
   //--- If objects already exist, simply refresh the camera
   if(m_are3DObjectsCreated)
     {
      //--- Auto-fit camera when enabled and data is available
      if(autoFitCamera && m_isDataLoaded)
         autoFitCameraPosition();

      //--- Apply updated camera transform
      updateCameraPosition();
      return true;
     }

   //--- Warn if this path is reached unexpectedly
   Print("WARNING: 3D objects not created - this shouldn't happen!");
   //--- Attempt to create objects as a fallback
   return create3DObjects();
  }

//+------------------------------------------------------------------+
//| Generate binomial sample, compute histogram and statistics       |
//+------------------------------------------------------------------+
bool loadDistributionData()
  {
   //--- Seed the random number generator with current tick count
   MathSrand(GetTickCount());

   //--- Allocate the sample data buffer
   ArrayResize(m_sampleData, sampleSize);
   //--- Fill the buffer with random binomial variates
   MathRandomBinomial(numTrials, successProbability, sampleSize, m_sampleData);

   //--- Compute the frequency histogram from the sample
   if(!computeHistogram(m_sampleData, m_histogramIntervals, m_histogramFrequencies,
                        m_maxDataValue, m_minDataValue, histogramCells))
     {
      Print("ERROR: Failed to calculate histogram");
      return false;
     }

   //--- Allocate arrays for the theoretical PMF curve
   ArrayResize(m_theoreticalXValues, numTrials + 1);
   ArrayResize(m_theoreticalYValues, numTrials + 1);
   //--- Fill X values as integer sequence 0 .. numTrials
   MathSequence(0, numTrials, 1, m_theoreticalXValues);
   //--- Compute binomial PMF for each X value
   MathProbabilityDensityBinomial(m_theoreticalXValues, numTrials, successProbability,
                                  false, m_theoreticalYValues);

   //--- Find the tallest histogram bin
   m_maxFrequency = m_histogramFrequencies[ArrayMaximum(m_histogramFrequencies)];
   //--- Find the peak theoretical PMF value
   m_maxTheoreticalValue = m_theoreticalYValues[ArrayMaximum(m_theoreticalYValues)];

   //--- Compute scaling factor to align histogram with PMF curve
   double scaleFactor = m_maxFrequency / m_maxTheoreticalValue;
   //--- Scale every bin frequency so it matches PMF units
   for(int i = 0; i < histogramCells; i++)
      m_histogramFrequencies[i] /= scaleFactor;

   //--- Compute all descriptive statistics
   computeAdvancedStatistics();

   //--- Mark data as successfully loaded
   m_isDataLoaded = true;

   //--- Update 3D bar heights if the scene is already built
   if(m_currentViewMode == VIEW_3D_MODE && m_are3DObjectsCreated)
     {
      //--- Refit camera to new data extents
      if(autoFitCamera)
         autoFitCameraPosition();
      //--- Reposition every bar in 3D space
      update3DHistogramBars();
     }

   Print("SUCCESS: Loaded distribution data");
   return true;
  }

Zunächst definieren wir die Funktion renderVisualization, die das Zeichnen je nach aktuellem Modus übernimmt. Dabei wird m_currentViewMode mit VIEW_2D_MODE verglichen, um entweder render2DVisualization oder andernfalls render3DVisualization aufzurufen, wodurch die Rendering-Logik für 2D- und 3D-Ansichten zentralisiert wird. Wir werden uns nicht groß mit 2D beschäftigen, da die Logik dieselbe ist wie bei der vorherigen Version. Für 3D implementieren wir die Funktion render3DVisualization, die vorzeitig zurückkehrt, wenn keine Daten vorliegen, die die Kamera und die Histogrammbalken aktualisiert, die eine Hintergrundfarbe aus backgroundTopColor, die in ARGB konvertiert wurde, festlegt, die die Szene mit Render unter Verwendung der Flags DX_CLEAR_COLOR | DX_CLEAR_DEPTH löscht, den Rahmen zeichnet, sofern er über draw3DBorder aktiviert ist, die Kopfzeile mit drawHeaderBarOn3D hinzufügt und den Vorgang mit einem Update beendet, um den 3D-Inhalt anzuzeigen. Um die Zeichenfläche einzurichten, erstellen wir die Funktion createCanvasAndObjects, rufen CreateBitmapLabel für m_mainCanvas im normalisierten ARGB-Format auf, legen die Objektabstände über ObjectSetInteger für OBJPROP_XDISTANCE und OBJPROP_YDISTANCE fest, und rufen anschließend initialize3DContext und create3DObjects auf und geben bei einem Fehler false zurück und geben den Fehler aus.

Anschließend definieren wir initialize3DContext, um die 3D-Umgebung zu konfigurieren: Wir legen die Projektionsmatrix mit ProjectionMatrixSet fest, wobei wir ein Sichtfeld von 30 Grad, das Seitenverhältnis aus den Abmessungen sowie Nah- und Fernebenen verwenden; wir definieren das Blickziel und die Aufwärtsrichtung über ViewTargetSet und ViewUpDirectionSet am Ursprung; wir legen Licht- und Umgebungsfarben mit LightColorSet und AmbientColorSet fest; wir passen die Kameraposition automatisch an, sofern diese Funktion aktiviert ist und die Daten geladen wurden, indem wir autoFitCameraPosition aufrufen; wir aktualisieren die Position mit updateCameraPosition; und geben den Erfolg aus.

Anschließend koordiniert create3DObjects die Erstellung von 3D-Elementen, indem es nacheinander create3DHistogramBars, createGroundPlane und bedingt create3DAxes, sofern show3DAxes wahr ist, aufruft, wobei bei Erfolg m_are3DObjectsCreated auf wahr gesetzt und eine Meldung ausgegeben wird, während wir bei einem Fehler eine Fehlermeldung ausdrucken und false zurückgeben. Zur Aktivierung des Modus implementieren wir setup3DMode, um zu prüfen, ob Objekte erstellt wurden; ist dies der Fall, werden die Kamera automatisch angepasst und aktualisiert; andernfalls wird eine Warnung ausgegeben und versucht, die Objekte über create3DObjects zu erstellen. Abschließend definieren wir loadDistributionData, um binomiale Daten vorzubereiten: Zufallsgenerator mit MathSrand unter Verwendung von GetTickCount, Anpassung der Größe von m_sampleData und Erzeugung von Stichproben über MathRandomBinomial, Berechnung des Histogramms mit computeHistogram, Festlegen der theoretischen Arrays mit MathSequence und MathProbabilityDensityBinomial, ermitteln die Maximalwerte mit ArrayMaximum, skalieren die Häufigkeiten so, dass sie dem theoretischen Maximum entsprechen, berechnen die Statistiken, setzen m_isDataLoaded auf true und, falls im VIEW_3D_MODE mit erstellten Objekten, passen die Kamera automatisch an, aktualisieren die Balken und geben eine Erfolgsmeldung aus. Um dies zu initialisieren, rufen wir die Funktionen wie folgt im Initialisierungs-Ereignis-Handler auf.

Einbindung des Initialisierungs-Ereignis-Handlers

Nachdem nun die gesamte Klassenlogik definiert ist, müssen wir sie mit dem Einstiegspunkt des Programms verknüpfen. In OnInit wird die Visualizer-Instanz erstellt, die Zeichenfläche und die dreidimensionalen Objekte werden eingerichtet, die Verteilungsdaten werden geladen und das erste Rendern wird ausgelöst. Jeder Fehler in dieser Phase wird sauber abgehandelt, indem die Instanz gelöscht und ein Initialisierungsfehlercode zurückgegeben wird, wodurch verhindert wird, dass das Programm in einem fehlerhaften Zustand ausgeführt wird.

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
DistributionVisualizer *distributionVisualizer = NULL; // Pointer to the active visualizer instance

//+------------------------------------------------------------------+
//| Initialise the EA, create the canvas and load distribution data  |
//+------------------------------------------------------------------+
int OnInit()
  {
   //--- Enable mouse movement events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE,  true);
   //--- Enable mouse wheel events on the chart
   ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);

   //--- Allocate and construct the visualizer object
   distributionVisualizer = new DistributionVisualizer();
   if(distributionVisualizer == NULL)
     {
      Print("ERROR: Failed to create window object");
      return INIT_FAILED;
     }

   //--- Create the canvas bitmap label and initialise the 3D scene
   if(!distributionVisualizer.createCanvasAndObjects())
     {
      Print("ERROR: Failed to create canvas");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Generate the binomial sample and compute all statistics
   if(!distributionVisualizer.loadDistributionData())
     {
      Print("ERROR: Failed to load distribution data");
      delete distributionVisualizer;
      distributionVisualizer = NULL;
      return INIT_FAILED;
     }

   //--- Render the initial frame
   distributionVisualizer.renderVisualization();
   ChartRedraw();

   Print("SUCCESS: Distribution window initialized");
   return INIT_SUCCEEDED;
  }

Zunächst deklarieren wir einen globalen Zeiger distributionVisualizer auf die Klasse DistributionVisualizer, der auf NULL initialisiert wird, um die Instanz des Tools im gesamten Programm zu verwalten. In OnInit aktivieren wir Mausereignisse, indem wir CHART_EVENT_MOUSE_MOVE und CHART_EVENT_MOUSE_WHEEL mit ChartSetInteger auf true setzen, um die Interaktion zu unterstützen. Wir instanziieren den Visualizer mit new, prüfen auf NULL und behandeln einen Fehler, indem wir eine Fehlermeldung ausgeben und INIT_FAILED zurückgeben. Als Nächstes rufen wir createCanvasAndObjects auf, um die Zeichenfläche und die 3D-Elemente einzurichten, einschließlich einer Fehlerbehandlung, die die Instanz löscht und im Fehlerfall einen Fehler zurückgibt. Anschließend laden wir die Daten über loadDistributionData – auch hier wieder mit einer Fehlerbehebung im Fehlerfall. Abschließend rendern wir die Visualisierung, zeichnen das Diagramm mit ChartRedraw neu, geben SUCCESS aus und geben INIT_SUCCEEDED zurück, um die Einrichtung zu bestätigen. Nach dem Kompilieren erhalten wir folgendes Ergebnis:

3D-BALKEN  INITIALISIERT

Nun müssen wir noch das Statistik- und Legendenfeld hinzufügen. Hier ist die Logik, mit der wir das erreichen.

Darstellung des Statistikfensters, der Legende und der Größenänderungsanzeige im dreidimensionalen Modus

Das Statistikfeld und die Legende wurden bereits in der zweidimensionalen Version implementiert und bieten einen erheblichen analytischen Mehrwert – sie zeigen dem Nutzer wichtige Kennzahlen wie Mittelwert, Standardabweichung, Konfidenzintervalle sowie eine Legende zum Vergleich des Histogramms mit der theoretischen Kurve. Im dreidimensionalen Modus müssen diese Elemente als zweidimensionale Überlagerungen beibehalten werden, die nach jedem Renderdurchgang über die dreidimensionale Szene gelegt werden. Die Größenänderungsanzeige muss ebenfalls immer dann erscheinen, wenn der Benutzer mit der Maus über einen Größenänderungsbereich fährt, unabhängig vom aktuellen Anzeigemodus. Dieser Block bindet diese Overlays in die dreidimensionale Rendering-Pipeline ein.

//+------------------------------------------------------------------+
//| Draw the statistics panel overlay on the 3D render               |
//+------------------------------------------------------------------+
void drawStatisticsPanelOn3D()
  {
   //--- Reuse the same 2D statistics panel for the 3D overlay
   drawStatisticsPanel();
  }
//+------------------------------------------------------------------+
//| Draw the legend panel overlay on the 3D render                   |
//+------------------------------------------------------------------+
void drawLegendOn3D()
  {
   //--- Compute legend panel absolute position (same layout as 2D)
   int legendX          = statsPanelX;
   int legendY          = HEADER_BAR_HEIGHT + statsPanelY + statsPanelHeight;
   int legendWidth      = statsPanelWidth;
   int legendHeightThis = legendHeight;

   //--- Compute legend background colour as a very light theme tint
   color legendBgColor = LightenColor(themeColor, 0.9);
   uchar bgAlpha       = 153;
   uint  argbLegendBg  = ColorToARGB(legendBgColor, bgAlpha);
   uint  argbBorder    = ColorToARGB(themeColor, 255);
   uint  argbText      = ColorToARGB(clrBlack, 255);

   //--- Flood-fill the legend background with alpha blending
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      for(int x = legendX; x <= legendX + legendWidth; x++)
         blendPixelSet(m_mainCanvas, x, y, argbLegendBg);

   //--- Draw all four border lines of the legend panel
   for(int x = legendX; x <= legendX + legendWidth; x++)
      blendPixelSet(m_mainCanvas, x, legendY, argbBorder); // Top border
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      blendPixelSet(m_mainCanvas, legendX + legendWidth, y, argbBorder); // Right border
   for(int x = legendX; x <= legendX + legendWidth; x++)
      blendPixelSet(m_mainCanvas, x, legendY + legendHeightThis, argbBorder); // Bottom border
   for(int y = legendY; y <= legendY + legendHeightThis; y++)
      blendPixelSet(m_mainCanvas, legendX, y, argbBorder); // Left border

   //--- Set the legend text font
   m_mainCanvas.FontSet("Arial", panelFontSize);

   //--- Initialise vertical text cursor inside the legend
   int itemY      = legendY + 10;
   int lineSpacing = panelFontSize;

   //--- Draw 3D histogram colour swatch and its label
   uint argbHist = ColorToARGB(histogramColor, 255);
   m_mainCanvas.FillRectangle(legendX + 7, itemY - 4, legendX + 22, itemY + 4, argbHist);
   m_mainCanvas.TextOut(legendX + 27, itemY - 4, "3D Histogram", argbText, TA_LEFT);
   itemY += lineSpacing;

   //--- Draw a short horizontal line as the curve colour swatch
   uint argbCurve = ColorToARGB(theoreticalCurveColor, 255);
   for(int i = 0; i < 15; i++)
     {
      blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY,     argbCurve); // Upper swatch pixel row
      blendPixelSet(m_mainCanvas, legendX + 7 + i, itemY + 1, argbCurve); // Lower swatch pixel row
     }
   //--- Draw the PMF curve label beside the swatch
   m_mainCanvas.TextOut(legendX + 27, itemY - 4, "Theoretical PMF", argbText, TA_LEFT);
  }
//+------------------------------------------------------------------+
//| Draw resize grip indicators overlaid on the 3D render            |
//+------------------------------------------------------------------+
void drawResizeIndicatorOn3D()
  {
   //--- Reuse the same 2D resize indicator for the 3D overlay
   drawResizeIndicator();
  }

//--- Call these in the 3D visual function

if(showStatistics)
  {
   drawStatisticsPanelOn3D();
   drawLegendOn3D();
  }
//--- Project and draw the theoretical PMF curve into screen space
draw3DTheoreticalCurve();

//--- Draw the resize grip indicator when hovering
if(m_isHoveringResizeZone && enableResizing)
   drawResizeIndicatorOn3D();

Hier definieren wir die Funktion drawStatisticsPanelOn3D, um Statistiken im 3D-Modus darzustellen, indem wir einfach drawStatisticsPanel aufrufen und dabei die 2D-Logik wiederverwenden, um die Konsistenz der Überlagerung zu gewährleisten. Für die Legende in 3D implementieren wir drawLegendOn3D ähnlich wie in 2D, jedoch mit einer Beschriftung „3D Histogram“: Wir legen die Positionen anhand der Eingaben fest, hellen themeColor für den Hintergrund mit LightenColor auf, bereiten ARGB-Farben einschließlich Alpha-Kanal über ColorToARGB vor, führen in einer Schleife mit blendPixelSet die Überblendung der Pixel für Füllung und Ränder durch, legen die Schriftart mit FontSet fest, zeichnen ein Beispielrechteck für das Histogramm mit FillRectangle und beschriften es mit TextOut, überblenden anschließend eine Beispielkurve und fügen deren Beschriftung hinzu, wodurch an 3D angepasste visuelle Schlüssel bereitgestellt werden. Um die Größenänderungsbereiche in 3D anzuzeigen, erstellen wir drawResizeIndicatorOn3D, das drawResizeIndicator aufruft, um das gleiche Feedback wie in 2D beizubehalten.

Im 3D-Rendering-Ablauf rufen wir, wenn showStatistics auf true gesetzt ist, die Funktionen drawStatisticsPanelOn3D und drawLegendOn3D auf, um Informationsfelder einzublenden; zeichnen die Kurve mit draw3DTheoreticalCurve für die probabilistische Überlagerung; und wenn der Mauszeiger über einen Größenänderungsbereich mit aktivierter Größenänderung bewegt wird, fügen wir den Indikator über drawResizeIndicatorOn3D hinzu, wobei wir 2D-Elemente nach dem 3D-Rendering integrieren, um eine hybride Darstellung zu erzielen. Nach der Kompilierung, erhalten wir folgendes Ergebnis.

STATISTIK- UND LEGENDENPANEL

Nachdem das Panel fertig ist, müssen wir uns nun um die Interaktionen mit dem Diagramm kümmern.

Umgang mit Mausinteraktionen und Umschalten des Anzeigemodus

Eine dreidimensionale Visualisierung ist nur so nützlich wie ihre Interaktivität. Der Benutzer muss die Szene drehen können, um die Balken aus verschiedenen Blickwinkeln zu betrachten, hinein- und herauszoomen können, um bestimmte Bereiche der Verteilung hervorzuheben, die Zeichenfläche verschieben können, um sie im Diagramm neu zu positionieren, ihre Größe anpassen können, um den Anzeigebereich einzustellen, und mit einem einzigen Klick zwischen dem zweidimensionalen und dem dreidimensionalen Modus wechseln können. All diese Interaktionen erfolgen über Mausereignisse, und jedes einzelne muss sorgfältig behandelt werden, um Konflikte zu vermeiden – beispielsweise sollte ein Ziehen, das zum Drehen der dreidimensionalen Szene gedacht ist, nicht gleichzeitig das Diagramm scrollen, und das Klicken auf das Umschaltsymbol sollte nicht gleichzeitig ein Ziehen auslösen. Dieser Block definiert die gesamte Interaktionslogik, die erforderlich ist, damit das Tool vollständig responsiv ist.

//+------------------------------------------------------------------+
//| Process mouse move and button events for interaction             |
//+------------------------------------------------------------------+
void handleMouseEvent(int mouseX, int mouseY, int mouseState)
  {
   //--- Snapshot previous hover states to detect changes
   bool previousHoverState       = m_isHoveringCanvas;
   bool previousHeaderHoverState = m_isHoveringHeader;
   bool previousResizeHoverState = m_isHoveringResizeZone;
   bool previousSwitchHoverState = m_isHoveringSwitchIcon;

   //--- Update canvas hover flag based on cursor position
   m_isHoveringCanvas = (mouseX >= m_currentPositionX &&
                         mouseX <= m_currentPositionX + m_currentWidth &&
                         mouseY >= m_currentPositionY &&
                         mouseY <= m_currentPositionY + m_currentHeight);

   //--- Update individual zone hover flags
   m_isHoveringHeader      = isMouseOverHeaderBar(mouseX, mouseY);
   m_isHoveringSwitchIcon  = isMouseOverSwitchIcon(mouseX, mouseY);
   m_isHoveringResizeZone  = isMouseInResizeZone(mouseX, mouseY, m_hoverResizeMode);

   //--- Determine if a redraw is needed due to hover state changes
   bool needRedraw = (previousHoverState       != m_isHoveringCanvas  ||
                      previousHeaderHoverState != m_isHoveringHeader  ||
                      previousResizeHoverState != m_isHoveringResizeZone ||
                      previousSwitchHoverState != m_isHoveringSwitchIcon);

   //--- Handle 3D orbit drag when in 3D mode and not over the header
   if(m_currentViewMode == VIEW_3D_MODE && m_isHoveringCanvas && !m_isHoveringHeader)
     {
      //--- Begin rotation on fresh left-button press
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         m_isRotating3D  = true;
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         //--- Prevent chart from consuming mouse scroll during rotation
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      //--- Continue rotation while button is held and dragging
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isRotating3D)
        {
         //--- Update azimuth angle proportional to horizontal mouse delta
         m_cameraAngleY += (mouseX - m_mouse3DStartX) / 300.0;
         //--- Update elevation angle proportional to vertical mouse delta
         m_cameraAngleX += (mouseY - m_mouse3DStartY) / 300.0;

         //--- Clamp elevation to avoid gimbal lock at poles
         if(m_cameraAngleX < -DX_PI * 0.49) m_cameraAngleX = -DX_PI * 0.49;
         if(m_cameraAngleX >  DX_PI * 0.49) m_cameraAngleX =  DX_PI * 0.49;

         //--- Update rotation anchor for the next delta computation
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         needRedraw = true;
        }
      //--- End rotation on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isRotating3D = false;
         //--- Restore chart scroll on release
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }

   //--- Handle button-press interactions
   if(mouseState == 1 && m_previousMouseButtonState == 0)
     {
      //--- Switch view mode when the icon is clicked
      if(m_isHoveringSwitchIcon)
        {
         switchViewMode();
         m_previousMouseButtonState = mouseState;
         return;
        }
      //--- Begin canvas drag when clicking the header (not a resize zone)
      else if(enableDragging && m_isHoveringHeader && !m_isHoveringResizeZone)
        {
         m_isDragging   = true;
         m_dragStartX   = mouseX;
         m_dragStartY   = mouseY;
         m_canvasStartX = m_currentPositionX;
         m_canvasStartY = m_currentPositionY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         needRedraw = true;
        }
      //--- Begin canvas resize when clicking a resize grip zone
      else if(m_isHoveringResizeZone)
        {
         m_isResizing          = true;
         m_activeResizeMode    = m_hoverResizeMode;
         m_resizeStartX        = mouseX;
         m_resizeStartY        = mouseY;
         m_resizeInitialWidth  = m_currentWidth;
         m_resizeInitialHeight = m_currentHeight;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
         needRedraw = true;
        }
     }
   //--- Continue drag or resize while button stays pressed
   else if(mouseState == 1 && m_previousMouseButtonState == 1)
     {
      if(m_isDragging)
         handleCanvasDrag(mouseX, mouseY);
      else if(m_isResizing)
         handleCanvasResize(mouseX, mouseY);
     }
   //--- End drag or resize on button release
   else if(mouseState == 0 && m_previousMouseButtonState == 1)
     {
      if(m_isDragging || m_isResizing)
        {
         m_isDragging       = false;
         m_isResizing       = false;
         m_activeResizeMode = NO_RESIZE;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
         needRedraw = true;
        }
     }

   //--- Redraw the visualization if any state changed
   if(needRedraw)
     {
      renderVisualization();
      ChartRedraw();
     }

   //--- Record current mouse position for next event
   m_lastMouseX = mouseX;
   m_lastMouseY = mouseY;
   //--- Record current button state for next event
   m_previousMouseButtonState = mouseState;
  }

//+------------------------------------------------------------------+
//| Handle mouse wheel zoom for the 3D scene                         |
//+------------------------------------------------------------------+
void handleMouseWheel(int mouseX, int mouseY, double delta)
  {
   //--- Determine if the wheel event occurred over the 3D canvas body
   bool isOverCanvas = (mouseX >= m_currentPositionX &&
                        mouseX <= m_currentPositionX + m_currentWidth &&
                        mouseY >= m_currentPositionY + HEADER_BAR_HEIGHT &&
                        mouseY <= m_currentPositionY + m_currentHeight);

   //--- Apply zoom only in 3D mode and when cursor is over the canvas
   if(m_currentViewMode == VIEW_3D_MODE && isOverCanvas)
     {
      //--- Suppress chart scroll so wheel is captured by the visualizer
      ChartSetInteger(0, CHART_MOUSE_SCROLL, false);

      //--- Adjust camera distance by a small fraction of the wheel delta
      m_cameraDistance *= 1.0 - delta * 0.001;

      //--- Clamp distance to prevent clipping through the scene
      if(m_cameraDistance <  20.0) m_cameraDistance =  20.0;
      if(m_cameraDistance > 200.0) m_cameraDistance = 200.0;

      //--- Re-render with updated camera distance
      renderVisualization();
      ChartRedraw();
     }
   else
     {
      //--- Restore chart scroll when wheel is outside the canvas
      ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
     }
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the mode switch icon         |
//+------------------------------------------------------------------+
bool isMouseOverSwitchIcon(int mouseX, int mouseY)
  {
   //--- Compute the icon's left edge from the canvas right margin
   int iconX = m_currentPositionX + m_currentWidth - SWITCH_ICON_SIZE - SWITCH_ICON_MARGIN;
   //--- Vertically centre the icon within the header bar
   int iconY = m_currentPositionY + (HEADER_BAR_HEIGHT - SWITCH_ICON_SIZE) / 2;

   //--- Return true if the cursor falls within the icon bounding box
   return (mouseX >= iconX && mouseX <= iconX + SWITCH_ICON_SIZE &&
           mouseY >= iconY && mouseY <= iconY + SWITCH_ICON_SIZE);
  }

//+------------------------------------------------------------------+
//| Toggle between 2D and 3D view modes                              |
//+------------------------------------------------------------------+
void switchViewMode()
  {
   //--- Switch from 2D to 3D
   if(m_currentViewMode == VIEW_2D_MODE)
     {
      m_currentViewMode = VIEW_3D_MODE;
      Print("Switched to 3D mode");

      //--- Set up the 3D scene; revert to 2D on failure
      if(!setup3DMode())
        {
         Print("ERROR: Failed to setup 3D mode, reverting to 2D");
         m_currentViewMode = VIEW_2D_MODE;
        }
      else
        {
         //--- Auto-fit camera to the scene on mode entry
         if(autoFitCamera)
            autoFitCameraPosition();
        }
     }
   else
     {
      //--- Switch from 3D back to 2D
      m_currentViewMode = VIEW_2D_MODE;
      Print("Switched to 2D mode");
     }

   //--- Render the scene in the new mode immediately
   renderVisualization();
   ChartRedraw();
  }

Wir definieren die Funktion handleMouseEvent, um alle Mausinteraktionen innerhalb des Visualizers zu verwalten. Wir speichern frühere Hover-Zustände, aktualisieren m_isHoveringCanvas, indem wir die Mausposition anhand der Canvas-Grenzen überprüfen, setzen m_isHoveringHeader auf isMouseOverHeaderBar, m_isHoveringSwitchIcon über isMouseOverSwitchIcon und m_isHoveringResizeZone mithilfe von isMouseInResizeZone.

Ein Neuzeichnungs-Flag wird ausgelöst, wenn sich einer der Hover-Zustände ändert. Im VIEW_3D_MODE über der Zeichenfläche, jedoch nicht über der Kopfzeile, wird die Drehung verarbeitet: Beim Drücken wird m_isRotating3D auf true gesetzt, die Startpunkte werden aufgezeichnet und das Scrollen wird mit ChartSetInteger für CHART_MOUSE_SCROLL deaktiviert; beim Ziehen passen wir m_cameraAngleY und m_cameraAngleX um mit 300,0 skalierte Deltas an, begrenzen den X-Winkel auf den Bereich zwischen -0,49PI und 0,49PI, um ein Umdrehen zu verhindern, aktualisieren die Startpunkte und setzen ein Flag für die Neuzeichnung; beim Loslassen die Rotation zurücksetzen und das Scrollen aktivieren. Anschließend prüfen wir die Tasten: Befindet sich der Cursor über dem Umschaltsymbol, rufen wir switchViewMode auf und kehren nach der Aktualisierung des Zustands zurück; befindet sich der Cursor über der Kopfzeile, ohne dass eine Größenänderung erfolgt, aktivieren wir das Ziehen mit starts und deaktivieren das Scrollen; befindet sich der Cursor über dem Größenänderungsbereich, aktivieren wir die Größenänderung, legen den Modus fest, erfassen die Anfangswerte und deaktivieren das Scrollen, wobei wir eine Neuzeichnung markieren. Bei gedrückter Taste handleCanvasDrag oder handleCanvasResize aufrufen; beim Loslassen Flag/Modus zurücksetzen, Scrollen aktivieren und Neuzeichnung anweisen. Bei Bedarf rufen wir renderVisualization und ChartRedraw auf und aktualisieren anschließend die letzte Mausposition und den Status.

Als Nächstes implementieren wir die Funktion handleMouseWheel für das Zoomen in 3D. Wir prüfen, ob sich der Mauszeiger im VIEW_3D_MODE über dem Darstellungsbereich unterhalb der Kopfzeile befindet, deaktivieren das Scrollen, multiplizieren m_cameraDistance mit 1,0 minus dem skalierten Delta-Wert für eine sanfte Anpassung, begrenzen den Wert auf einen Bereich zwischen 20,0 und 200,0 und führen mit redraw ein erneutes Rendern durch; andernfalls aktivieren wir das Scrollen, um die Navigation im Diagramm zu ermöglichen.

Um das Hover-Ereignis des Umschaltknopfes zu erkennen, erstellen wir die Funktion isMouseOverSwitchIcon, die die Koordinaten des Symbols anhand der aktuellen Abmessungen und Konstanten berechnet und true zurückgibt, wenn sich der Mauszeiger innerhalb der quadratischen Begrenzungen befindet. Abschließend definieren wir die Funktion switchViewMode zum Umschalten zwischen den Modi: Bei 2D wird VIEW_3D_MODE gesetzt, die Umschaltung ausgegeben, setup3DMode versucht und bei Fehlschlag der ursprüngliche Zustand wiederhergestellt bzw. ein Fehler ausgegeben; andernfalls wird die Kamera automatisch angepasst, sofern diese Funktion aktiviert ist; im 3D-Modus wird auf 2D umgeschaltet und gedruckt. Anschließend werden wir den neuen Modus rendern und das Diagramm neu zeichnen, um es sofort zu aktualisieren. Wir können diese Funktionen nun im Ereignis-Handler des Diagramms aufrufen.

//+------------------------------------------------------------------+
//| Route chart mouse and wheel events to the visualizer             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Abort if the visualizer has not been initialised
   if(distributionVisualizer == NULL) return;

   //--- Handle mouse move and button events
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX     = (int)lparam;                        // Horizontal cursor position in pixels
      int mouseY     = (int)dparam;                        // Vertical cursor position in pixels
      int mouseState = (int)StringToInteger(sparam);       // Bitmask of pressed mouse buttons

      distributionVisualizer.handleMouseEvent(mouseX, mouseY, mouseState);
     }

   //--- Handle mouse wheel scroll events
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Unpack the cursor X from the low 16 bits of lparam
      int mouseX = (int)(short) lparam;
      //--- Unpack the cursor Y from the high 16 bits of lparam
      int mouseY = (int)(short)(lparam >> 16);

      distributionVisualizer.handleMouseWheel(mouseX, mouseY, dparam);
     }
  }

In der Ereignisbehandlung von OnChartEvent verarbeiten wir Interaktionen mit dem Diagramm global und brechen den Ablauf vorzeitig ab, wenn distributionVisualizer NULL ist, um Fehler zu vermeiden. Wenn die ID CHARTEVENT_MOUSE_MOVE lautet, wandeln wir die Parameter um, um die Mauskoordinaten und den Mausstatus zu ermitteln, und delegieren die Verarbeitung anschließend an die Methode handleMouseEvent der Visualizer-Instanz. Für CHARTEVENT_MOUSE_WHEEL extrahieren wir die angepassten Mauspositionen aus den lparam-Bits und rufen handleMouseWheel mit dem Delta-Wert auf, wodurch das Zoomen per Mausrad in 3D ermöglicht wird. Wir können nun den Handler für die Deinitialisierung und den Tick-Ereignis-Handler aktualisieren, damit die Änderungen im folgenden Format wirksam werden.

//+------------------------------------------------------------------+
//| Release all resources when the EA is removed                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Destroy the visualizer object if it still exists
   if(distributionVisualizer != NULL)
     {
      delete distributionVisualizer;
      distributionVisualizer = NULL;
     }

   //--- Redraw the chart to remove the canvas bitmap object
   ChartRedraw();
   Print("Distribution window deinitialized");
  }

//+------------------------------------------------------------------+
//| Reload distribution data on each new bar                         |
//+------------------------------------------------------------------+
void OnTick()
  {
   //--- Track the last processed bar open time across calls
   static datetime lastBarTimestamp = 0;
   //--- Read the current bar open time on the configured timeframe
   datetime currentBarTimestamp = iTime(_Symbol, chartTimeframe, 0);

   //--- Reload and redraw only when a new bar has formed
   if(currentBarTimestamp > lastBarTimestamp)
     {
      if(distributionVisualizer != NULL)
        {
         //--- Regenerate the binomial sample and statistics
         if(distributionVisualizer.loadDistributionData())
           {
            //--- Redraw the updated visualization
            distributionVisualizer.renderVisualization();
            ChartRedraw();
           }
        }
      //--- Update the bar timestamp to prevent repeated processing
      lastBarTimestamp = currentBarTimestamp;
     }
  }

In OnDeinit prüfen wir, ob distributionVisualizer nicht NULL ist, löschen die Instanz, um Ressourcen freizugeben, setzen den Zeiger auf NULL, zeichnen das Diagramm mit ChartRedraw neu und geben eine Deinitialisierungsmeldung aus. Als Nächstes verwenden wir in OnTick eine statische Variable lastBarTimestamp, um den Eröffnungszeitpunkt des vorherigen Balkens zu erfassen, und ermitteln den aktuellen Zeitpunkt über iTime unter Angabe des Symbols, des chartTimeframe und des Offsets 0. Wird ein neuer Balken erkannt, prüfen wir, ob der Visualizer vorhanden ist, laden die Daten mit loadDistributionData neu, führen bei Erfolg eine erneute Darstellung über renderVisualization durch, zeichnen das Diagramm neu und aktualisieren den Zeitstempel. Der vollständige Interaktionszyklus sieht wie folgt aus.

INTERAKTIONSZYKLUS

Damit ist unsere Implementierung für die 3D-Visualisierung nun abgeschlossen. Nun bleibt nur noch, die Funktionsfähigkeit des Systems zu testen, was im folgenden Abschnitt behandelt wird.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die Visualisierung in einer einzigen GIF-Animation (Graphics Interchange Format).

BACKTEST-GIF

Während der Tests wurden die Histogrammbalken bei unterschiedlicher Anzahl von Versuchen dreidimensional präzise skaliert, die automatische Anpassung der Kamera positionierte die Ansicht beim Laden stets so, dass die Balken vollständig sichtbar waren, und der Wechsel zwischen zweidimensionaler und dreidimensionaler Darstellung erfolgte ohne Datenverlust oder Störungen im Layout.


Schlussfolgerung

In diesem Artikel haben wir DirectX-3D in den MQL5-Viewer für die Binomialverteilung integriert und damit umschaltbare 2D-/3D-Modi sowie Kamerasteuerung für Drehung, Zoom und automatische Anpassung ermöglicht. Wir haben 3D-Histogrammbalken mit einer Bodenebene und farbcodierten Achsen gerendert, die theoretische PMF-Kurve in den perspektivischen Raum projiziert und 2D-Elemente wie Statistikpanel, Legenden und anpassbare Designs beibehalten. Die Implementierungsdetails umfassten eine klassenbasierte Architektur, Mausinteraktionen, Echtzeit-Aktualisierungen bei neuen Balken sowie konfigurierbare Eingaben für Versuche, Wahrscheinlichkeit, Stichprobengröße und Anzeigeeinstellungen. Nach der Lektüre dieses Artikels können Sie:

  • direkt im Diagramm zwischen der zweidimensionalen und der dreidimensionalen Darstellung von Binomialverteilungen wechseln, ohne das Programm neu zu starten
  • das dreidimensionale Histogramm drehen und zoomen, um die Formen der Wahrscheinlichkeitsmassenfunktion und die Häufigkeitskontraste aus jedem beliebigen Winkel zu untersuchen
  • die dreidimensionale Visualisierung zusammen mit der überlagerten theoretischen Kurve verwenden, um simulierte Stichprobenverteilungen in Echtzeit mit den erwarteten binomialen Wahrscheinlichkeiten zu vergleichen.

In den nächsten Abschnitten werden wir untersuchen, wie wir eine Schwenkfunktion zum Verschieben der 3D-Ansicht hinzufügen, weitere statistische Verteilungsfunktionen in unsere 2D-Balkendiagramme integrieren und einen nahtlosen Wechsel ermöglichen können. Bleiben Sie dran!

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

Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 63): Automatisierung der Erkennung von steigenden und fallenden Keilen in MQL5 Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 63): Automatisierung der Erkennung von steigenden und fallenden Keilen in MQL5
In diesem Teil der Reihe Entwicklung des Price-Action-Analyse-Toolkits entwickeln wir einen MQL5-Indikator, der steigende und fallende Keilformationen automatisch in Echtzeit erkennt. Das System überprüft Pivot-Strukturen, validiert die Konvergenz der Begrenzungslinien mathematisch, verhindert überlappende Formationen und überwacht die Bedingungen für Ausbruch und Scheitern mit präziser visueller Rückmeldung. Diese auf einer klaren, objektorientierten Architektur basierende Implementierung wandelt die subjektive Erkennung von Keilformationen in eine strukturierte, zustandsabhängige Analysekomponente um, die darauf ausgelegt ist, eine disziplinierte Price-Action-Analyse zu unterstützen.
Implementierung des Truncated-Newton-Conjugate-Gradient-Algorithmus in MQL5 Implementierung des Truncated-Newton-Conjugate-Gradient-Algorithmus in MQL5
Dieser Artikel implementiert einen Truncated-Newton-Conjugate-Gradient (TNC) Optimierer mit Box-Beschränkungen in MQL5 und beschreibt detailliert seine Kernkomponenten: Skalierung, Projektion auf Grenzen, Liniensuche und Hessematrix-Vektor-Produkte mittels finiter Differenzen. Es stellt eine Zielfunktions-Wrapperklasse bereit, die analytische oder numerische Ableitungen unterstützt, und validiert die Implementierung am Rosenbrock-Benchmark. Ein Beispiel für eine logistische Regression veranschaulicht, wie TNC als weitgehend austauschbare Alternative zu LBFGS eingesetzt werden kann.
PD-Arrays meistern: Optimierung des Handels mit Ungleichgewichten in PD-Arrays PD-Arrays meistern: Optimierung des Handels mit Ungleichgewichten in PD-Arrays
Dieser Artikel befasst sich mit einem spezialisierten, trendfolgenden EA, der deutlich machen soll, wie sich Setups, die sich aus Ungleichgewichten in Premium-/Discount-Bereichen (PD-Arrays) ergeben, definieren und nutzen lassen. In diesem Artikel wird ein EA ausführlich vorgestellt, der speziell für Trader entwickelt wurde, die PD-Arrays und Ungleichgewichte als Einstiegskriterien für ihre Trades und Handelsentscheidungen optimieren und nutzen möchten. Außerdem wird untersucht, wie sich Premium- und Discount-Zonen korrekt ermitteln und charakterisieren lassen und wie man sie jeweils validieren und nutzen kann, wenn sie unter den entsprechenden Marktbedingungen auftreten, um so die sich aus solchen Szenarien ergebenden Chancen bestmöglich zu nutzen.
Trading-Disziplin im Code verankern (Teil 3): Durchsetzung symbolbezogener Handelsgrenzen mit einer Whitelist in MQL5 Trading-Disziplin im Code verankern (Teil 3): Durchsetzung symbolbezogener Handelsgrenzen mit einer Whitelist in MQL5
In diesem Artikel wird ein MQL5-Framework beschrieben, das den Handel auf eine genehmigte Auswahl von Symbolen beschränkt. Die Lösung kombiniert eine gemeinsam genutzte Bibliothek, ein Konfigurations-Dashboard und einen Expert Advisor zur Durchsetzung, der jeden Handel anhand einer Whitelist überprüft und blockierte Versuche protokolliert. Sie enthält voll funktionsfähige Code-Beispiele, eine klare Erläuterung der strukturellen Entwurfsentscheidungen sowie Validierungstests, die eine zuverlässige Symbolfilterung, eine kontrollierte Marktpräsenz und eine transparente Überwachung der Regelumsetzung bestätigen.