Galerie der Oberflächendiagramme

Galerie mit drei verschiedenen Möglichkeiten, ein Surface3D Diagramm zu verwenden.

DieOberflächengrafik-Galerie demonstriert drei verschiedene benutzerdefinierte Funktionen mit Surface3D Graphen. Die Funktionen haben ihre eigenen Registerkarten in der Anwendung.

Die folgenden Abschnitte konzentrieren sich nur auf diese Funktionen und übergehen die Erläuterung der grundlegenden Funktionalität - eine ausführlichere QML-Beispieldokumentation finden Sie unter Simple Scatter Graph.

Ausführen des Beispiels

Um das Beispiel auszuführen Qt Creatorzu starten, öffnen Sie den Modus Welcome und wählen Sie das Beispiel unter Examples aus. Weitere Informationen finden Sie unter Erstellen und Ausführen eines Beispiels.

Höhenkarte

Erstellen Sie auf der Registerkarte Height Map ein Oberflächendiagramm aus Höhendaten. Die verwendeten Daten sind eine Höhenkarte von Mount Ruapehu und Mount Ngauruhoe in Neuseeland.

Hinzufügen von Daten zum Diagramm

Die Daten werden mit HeightMapSurfaceDataProxy gesetzt, das Höheninformationen aus einem Höhenkartenbild liest. Der Proxy selbst ist in einer Surface3DSeries enthalten. Innerhalb der HeightMapSurfaceDataProxy gibt die Eigenschaft heightMapFile die Bilddatei an, die die Höhendaten enthält. Die Werteigenschaften im Proxy definieren die minimalen und maximalen Werte für die Breite, Tiefe und Höhe des Oberflächenbereichs. Die Werte z und x sind in Breiten- und Längengraden, ungefähr an der realen Position, und y ist in Metern angegeben.

Hinweis: Das Seitenverhältnis des Diagramms wird nicht auf den realen Maßstab eingestellt, sondern die Höhe wird übertrieben dargestellt.

Surface3DSeries {
    id: heightSeries
    flatShadingEnabled: false
    drawMode: Surface3DSeries.DrawSurface

    HeightMapSurfaceDataProxy {
        heightMapFile: "://qml/qmlsurfacegallery/heightmap.png"
        // We don't want the default data values set by heightmap proxy, but use
        // actual coordinate and height values instead
        autoScaleY: true
        minYValue: 740
        maxYValue: 2787
        minZValue: -374 // ~ -39.374411"N
        maxZValue: -116 // ~ -39.115971"N
        minXValue: 472  // ~ 175.471767"E
        maxXValue: 781  // ~ 175.780758"E
    }

    onDrawModeChanged: heightMapView.checkState()
}
Anzeige der Daten

Richten Sie in main.qml das Element Surface3D für die Anzeige der Daten ein.

Definieren Sie zunächst den benutzerdefinierten Farbverlauf, der für die Oberfläche verwendet werden soll. Stellen Sie die Farben von Position 0.0 bis 1.0 mit ColorGradient ein, mit zwei zusätzlichen Stopps, um die Grafik lebendiger zu machen:

ColorGradient {
    id: surfaceGradient
    ColorGradientStop { position: 0.0; color: "darkgreen"}
    ColorGradientStop { position: 0.15; color: "darkslategray" }
    ColorGradientStop { position: 0.7; color: "peru" }
    ColorGradientStop { position: 1.0; color: "white" }
}

Setzen Sie dieses Element in die Eigenschaft baseGradients in dem in Surface3D verwendeten theme:

theme: Theme3D {
    type: Theme3D.ThemeStoneMoss
    font.family: "STCaiyun"
    font.pointSize: 35
    colorStyle: Theme3D.ColorStyleRangeGradient
    baseGradients: [surfaceGradient] // Use the custom gradient
}

Verwenden Sie die Schaltflächen, um andere Funktionen von Surface3D zu steuern.

Die erste Schaltfläche schaltet das Oberflächenraster ein und aus. Der Zeichenmodus kann nicht vollständig deaktiviert werden, d.h. wenn die Oberfläche selbst nicht sichtbar ist, kann das Oberflächenraster nicht ausgeblendet werden:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawWireframe)
        heightSeries.drawMode &= ~Surface3DSeries.DrawWireframe;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawWireframe;
}

Mit der zweiten Schaltfläche wird die Farbe des Oberflächengitters eingestellt:

onClicked: {
    if (Qt.colorEqual(heightSeries.wireframeColor, "#000000")) {
        heightSeries.wireframeColor = "red";
        text = "Black surface\ngrid color";
    } else {
        heightSeries.wireframeColor = "black";
        text = "Red surface\ngrid color";
    }
}

Die dritte schaltet die Oberfläche im Oberflächen-Zeichenmodus ein oder aus. Der Zeichenmodus kann nicht vollständig deaktiviert werden, d.h. wenn das Oberflächengitter nicht sichtbar ist, kann die Oberfläche selbst nicht ausgeblendet werden:

onClicked: {
    if (heightSeries.drawMode & Surface3DSeries.DrawSurface)
        heightSeries.drawMode &= ~Surface3DSeries.DrawSurface;
    else
        heightSeries.drawMode |= Surface3DSeries.DrawSurface;
}

Mit dem vierten wird der Schattierungsmodus eingestellt. Wenn Sie das Beispiel auf einem OpenGL ES-System ausführen, ist Flat Shading nicht verfügbar:

onClicked: {
    if (heightSeries.flatShadingEnabled) {
        heightSeries.flatShadingEnabled = false;
        text = "Show\nFlat"
    } else {
        heightSeries.flatShadingEnabled = true;
        text = "Show\nSmooth"
    }
}

Mit den übrigen Schaltflächen werden die Hintergrundfunktionen des Diagramms gesteuert.

Spektrogramm

Auf der Registerkarte Spectrogram können Sie polare und kartesische Spektrogramme anzeigen und die orthografische Projektion verwenden, um sie in 2D darzustellen.

Ein Spektrogramm ist ein Oberflächendiagramm mit einem Bereichsgradienten, der zur Hervorhebung der verschiedenen Werte verwendet wird. Normalerweise werden Spektrogramme mit zweidimensionalen Oberflächen dargestellt, was durch eine orthografische Ansicht des Graphen von oben nach unten simuliert wird. Um den 2D-Effekt zu verstärken, deaktivieren Sie die Drehung des Diagramms mit der Maus oder durch Berühren, wenn Sie sich im orthografischen Modus befinden.

Erstellen eines Spektrogramms

Um ein 2D-Spektrogramm zu erstellen, definieren Sie ein Element Surface3D mit den in Surface3DSeries angegebenen Daten mit einem ItemModelSurfaceDataProxy:

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        flatShadingEnabled: false
        drawMode: Surface3DSeries.DrawSurface
        baseGradient: surfaceGradient
        colorStyle: Theme3D.ColorStyleRangeGradient
        itemLabelFormat: "(@xLabel, @zLabel): @yLabel"

        ItemModelSurfaceDataProxy {
            itemModel: surfaceData.model
            rowRole: "radius"
            columnRole: "angle"
            yPosRole: "value"
        }
    }

Die wichtigsten Eigenschaften zur Aktivierung des 2D-Effekts sind orthoProjection und scene.activeCamera.cameraPreset. Entfernen Sie die Perspektive, indem Sie die orthografische Projektion für das Diagramm aktivieren, und die Y-Dimension, indem Sie das Diagramm direkt von oben betrachten:

// Remove the perspective and view the graph from top down to achieve 2D effect
orthoProjection: true
scene.activeCamera.cameraPreset: Camera3D.CameraPresetDirectlyAbove

Da bei dieser Betrachtungsweise das Gitter der horizontalen Achse größtenteils von der Oberfläche verdeckt wird, kippen Sie das horizontale Gitter, damit es über dem Diagramm gezeichnet wird:

flipHorizontalGrid: true
Polares Spektrogramm

Je nach Datenlage ist es manchmal sinnvoller, ein Polardiagramm anstelle eines kartesischen Diagramms zu verwenden. Dies wird durch die Eigenschaft polar unterstützt.

Fügen Sie eine Schaltfläche hinzu, um zwischen dem polaren und dem kartesischen Modus zu wechseln:

Button {
    id: polarToggle
    anchors.margins: 5
    anchors.left: parent.left
    anchors.top: parent.top
    width: spectrogramView.buttonWidth // Calculated elsewhere based on screen orientation
    text: "Switch to\n" + (surfaceGraph.polar ? "cartesian" : "polar")
    onClicked: surfaceGraph.polar = !surfaceGraph.polar;
}

Im polaren Modus wird die X-Achse in die Winkelpolarachse und die Z-Achse in eine radiale Polarachse umgewandelt. Die Oberflächenpunkte werden entsprechend den neuen Achsen neu berechnet.

Die Radialachsenbeschriftungen werden standardmäßig außerhalb des Diagramms gezeichnet. Um sie direkt neben der 0-Grad-Winkelachse innerhalb des Diagramms zu zeichnen, definieren Sie nur einen kleinen Versatz für sie:

radialLabelOffset: 0.01

Um den 2D-Effekt zu erzwingen, deaktivieren Sie die Drehung des Diagramms im orthografischen Modus, indem Sie die Standardeingabe mit einer benutzerdefinierten Eingabehandlung überschreiben, die die Eigenschaft rotationEnabled automatisch auf der Grundlage des Projektionsmodus umschaltet:

inputHandler: TouchInputHandler3D {
    rotationEnabled: !surfaceGraph.orthoProjection
}

Oscilloscope

Kombinieren Sie auf der Registerkarte Oscilloscope C++ und QML in einer Anwendung, und zeigen Sie Daten an, die sich dynamisch ändern.

Datenquelle in C++

Die auf dem Elementmodell basierenden Proxies eignen sich gut für einfache oder statische Diagramme, aber verwenden Sie grundlegende Proxies, um die beste Leistung bei der Anzeige von sich in Echtzeit ändernden Daten zu erzielen. Diese werden in QML nicht unterstützt, da die von ihnen gespeicherten Datenelemente nicht QObject erben und daher nicht direkt von QML-Code aus manipuliert werden können. Um diese Einschränkung zu überwinden, implementieren Sie eine einfache DataSource Klasse in C++, um den Daten-Proxy der Serie zu füllen.

Erstellen Sie eine Klasse DataSource, um zwei Methoden bereitzustellen, die von QML aus aufgerufen werden können:

class DataSource : public QObject
{
    Q_OBJECT
    ...
Q_INVOKABLE void generateData(int cacheCount, int rowCount, int columnCount,
                              float xMin, float xMax,
                              float yMin, float yMax,
                              float zMin, float zMax);

Q_INVOKABLE void update(QSurface3DSeries *series);

Die erste Methode, generateData(), erstellt einen Cache mit simulierten Oszilloskopdaten, die angezeigt werden sollen. Die Daten werden in einem Format zwischengespeichert, das von QSurfaceDataProxy akzeptiert wird:

// Populate caches
auto *generator = QRandomGenerator::global();
for (int i = 0; i < cacheCount; ++i) {
    QSurfaceDataArray &cache = m_data[i];
    float cacheXAdjustment = cacheStep * i;
    float cacheIndexAdjustment = cacheIndexStep * i;
    for (int j = 0; j < rowCount; ++j) {
        QSurfaceDataRow &row = *(cache[j]);
        float rowMod = (float(j)) / float(rowCount);
        float yRangeMod = yRange * rowMod;
        float zRangeMod = zRange * rowMod;
        float z = zRangeMod + zMin;
        qreal rowColWaveAngleMul = M_PI * M_PI * rowMod;
        float rowColWaveMul = yRangeMod * 0.2f;
        for (int k = 0; k < columnCount; k++) {
            float colMod = (float(k)) / float(columnCount);
            float xRangeMod = xRange * colMod;
            float x = xRangeMod + xMin + cacheXAdjustment;
            float colWave = float(qSin((2.0 * M_PI * colMod) - (1.0 / 2.0 * M_PI)) + 1.0);
            float y = (colWave * ((float(qSin(rowColWaveAngleMul * colMod) + 1.0))))
                    * rowColWaveMul
                    + generator->bounded(0.15f) * yRangeMod;

            int index = k + cacheIndexAdjustment;
            if (index >= columnCount) {
                // Wrap over
                index -= columnCount;
                x -= xRange;
            }
            row[index] = QVector3D(x, y, z);
        }
    }
}

Die zweite Methode, update(), kopiert einen Satz der zwischengespeicherten Daten in ein anderes Array, das durch den Aufruf von QSurfaceDataProxy::resetArray() auf den Daten-Proxy der Serie gesetzt wird. Um den Overhead zu minimieren, sollten Sie dasselbe Array wiederverwenden, wenn sich die Abmessungen des Arrays nicht geändert haben:

// Each iteration uses data from a different cached array
if (++m_index >= m_data.size())
    m_index = 0;

const QSurfaceDataArray &array = m_data.at(m_index);
int newRowCount = array.size();
int newColumnCount = array.at(0)->size();

// If the first time or the dimensions of the cache array have changed,
// reconstruct the reset array
if (!m_resetArray || series->dataProxy()->rowCount() != newRowCount
        || series->dataProxy()->columnCount() != newColumnCount) {
    m_resetArray = new QSurfaceDataArray();
    m_resetArray->reserve(newRowCount);
    for (int i = 0; i < newRowCount; ++i)
        m_resetArray->append(new QSurfaceDataRow(newColumnCount));
}

// Copy items from our cache to the reset array
for (int i = 0; i < newRowCount; ++i) {
    const QSurfaceDataRow &sourceRow = *(array.at(i));
    QSurfaceDataRow &row = *(*m_resetArray)[i];
    std::copy(sourceRow.cbegin(), sourceRow.cend(), row.begin());
}

// Notify the proxy that data has changed
series->dataProxy()->resetArray(m_resetArray);

Auch wenn wir mit dem Array-Zeiger arbeiten, der zuvor auf den Proxy gesetzt wurde, muss QSurfaceDataProxy::resetArray() immer noch aufgerufen werden, nachdem die Daten im Array geändert wurden, um das Diagramm zum Rendern der Daten aufzufordern.

Um von QML aus auf die Methoden von DataSource zugreifen zu können, legen Sie die Datenquelle offen, indem Sie aus der DataSource eine QML_ELEMENT machen:

class DataSource : public QObject
{
    Q_OBJECT
    QML_ELEMENT

Deklarieren Sie sie außerdem als QML-Modul in der Datei CMakeLists.txt:

qt6_add_qml_module(qmlsurfacegallery
    URI SurfaceGallery
    VERSION 1.0
    NO_RESOURCE_TARGET_PATH
    SOURCES
        datasource.cpp datasource.h
    ...
)

Um QSurface3DSeries Zeiger als Parameter für die Methoden der Klasse DataSource in allen Umgebungen und Builds zu verwenden, stellen Sie sicher, dass der Metatyp registriert ist:

qRegisterMetaType<QSurface3DSeries *>();
QML-Anwendung

Um DataSource zu verwenden, importieren Sie das QML-Modul und erstellen Sie eine Instanz von DataSource, die verwendet werden soll:

import SurfaceGallery
...
DataSource {
    id: dataSource
}

Definieren Sie einen Surface3D Graphen und geben Sie ihm einen Surface3DSeries:

Surface3D {
    id: surfaceGraph
    anchors.fill: parent

    Surface3DSeries {
        id: surfaceSeries
        drawMode: Surface3DSeries.DrawSurfaceAndWireframe
        itemLabelFormat: "@xLabel, @zLabel: @yLabel"

Geben Sie keinen Proxy für die Surface3DSeries an, die Sie an das Diagramm anhängen. Dadurch wird für die Reihe die Standardeinstellung QSurfaceDataProxy verwendet.

Blenden Sie die Elementbeschriftung mit itemLabelVisible aus. Bei dynamischen, sich schnell ändernden Daten wäre eine schwebende Auswahlbeschriftung ablenkend und schwer zu lesen.

itemLabelVisible: false

Sie können die Informationen zum ausgewählten Element in einem Text -Element anstelle der standardmäßigen schwebenden Beschriftung über dem Auswahlzeiger anzeigen:

onItemLabelChanged: {
    if (surfaceSeries.selectedPoint == surfaceSeries.invalidSelectionPosition)
        selectionText.text = "No selection";
    else
        selectionText.text = surfaceSeries.itemLabel;
}

Initialisieren Sie den Cache DataSource, wenn der Graph vollständig ist, indem Sie eine Hilfsfunktion generateData() aufrufen, die die gleichnamige Methode in DataSource aufruft:

Component.onCompleted: oscilloscopeView.generateData();
...
function generateData() {
    dataSource.generateData(oscilloscopeView.sampleCache, oscilloscopeView.sampleRows,
                            oscilloscopeView.sampleColumns,
                            surfaceGraph.axisX.min, surfaceGraph.axisX.max,
                            surfaceGraph.axisY.min, surfaceGraph.axisY.max,
                            surfaceGraph.axisZ.min, surfaceGraph.axisZ.max);
}

Um die Aktualisierungen der Daten auszulösen, definieren Sie eine Timer, die die Methode update() in DataSource in den gewünschten Intervallen aufruft:

Timer {
    id: refreshTimer
    interval: 1000 / frequencySlider.value
    running: true
    repeat: true
    onTriggered: dataSource.update(surfaceSeries);
}
Aktivieren des direkten Rendering

Da diese Anwendung potenziell mit vielen sich schnell ändernden Daten arbeitet, verwendet sie aus Leistungsgründen den direkten Rendering-Modus. Um Antialiasing in diesem Modus zu aktivieren, ändern Sie das Oberflächenformat des Anwendungsfensters. Das von QQuickView verwendete Standardformat unterstützt kein Antialiasing. Verwenden Sie die mitgelieferte Utility-Funktion, um das Oberflächenformat in main.cpp zu ändern:

#include <QtDataVisualization/qutils.h>
...
// Enable antialiasing in direct rendering mode
viewer.setFormat(qDefaultSurfaceFormat(true));

Beispiel Inhalt

Beispielprojekt @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.