Auf dieser Seite

Achsbehandlung

Implementierung des Ziehens von Achsen mit einem benutzerdefinierten Eingabe-Handler in QML und Erstellung eines benutzerdefinierten Achsenformatierers.

„Axis Handling“ demonstriert zwei verschiedene benutzerdefinierte Funktionen für Achsen. Die Funktionen verfügen über eigene Registerkarten in der Anwendung.

Die folgenden Abschnitte konzentrieren sich ausschließlich auf diese Funktionen und verzichten auf eine Erläuterung der Grundfunktionalität – eine ausführlichere Dokumentation mit QML-Beispielen finden Sie unter „Einfaches Streudiagramm“.

3D-Streudiagramm mit Kugeln und datumsbasierten Achsen

Ausführen des Beispiels

Sie können das Beispiel wie folgt ausführen:

Achsenverschieben

Implementieren Sie auf der Registerkarte „ Axis Dragging “ einen benutzerdefinierten Eingabehandler in QML, mit dem Sie Achsenbeschriftungen ziehen können, um die Achsenbereiche zu ändern. Verwenden Sie außerdem die orthogonale Projektion und aktualisieren Sie die Eigenschaften eines benutzerdefinierten Elements dynamisch.

Überschreiben der Standard-Eingabeverarbeitung

Um den Standardmechanismus zur Eingabeverarbeitung zu deaktivieren, heben Sie die Einstellung des Standard-Eingabehandlers von „ Scatter3D “ auf:

unsetDefaultInputHandler();
    ...

Fügen Sie anschließend ein „ MouseArea “ hinzu und legen Sie fest, dass es das übergeordnete Element ausfüllt – dies ist dasselbe „ Item “, in dem sich unser „ scatterGraph “ befindet. Stellen Sie außerdem ein, dass nur Klicks mit der linken Maustaste akzeptiert werden, da in diesem Beispiel die anderen Tasten nicht benötigt werden:

MouseArea {
    anchors.fill: parent
    hoverEnabled: true
    acceptedButtons: Qt.LeftButton
    ...

Hören Sie anschließend auf Mausklicks und senden Sie bei Erfassung eine Auswahlabfrage an den Graphen:

onPressed: (mouse)=> {
               scatterGraph.doPicking(Qt.point(mouse.x, mouse.y));
           }

Der Signal-Handler „ onPositionChanged “ erfasst die aktuelle Mausposition, die für die Berechnung der Bewegungsstrecke benötigt wird:

onPositionChanged: (mouse)=> {
                       currentMouseX = mouse.x;
                       currentMouseY = mouse.y;
    ...

Speichern Sie am Ende von „ onPositionChanged “ die vorherige Mausposition für die Berechnung der Bewegungsstrecke, die später vorgestellt wird:

...
    previousMouseX = currentMouseX;
    previousMouseY = currentMouseY;
}
Umwandlung der Mausbewegung in eine Änderung des Achsenbereichs

Hören Sie im Signal „ scatterGraph “ auf das Signal „ onSelectedElementChanged “. Das Signal wird ausgegeben, nachdem die Auswahlabfrage im Signal „ onPressed “ des Signals „ inputArea “ erfolgt ist. Weisen Sie den Elementtyp einer von Ihnen in der Hauptkomponente definierten Eigenschaft (property int selectedAxisLabel: -1) zu, da es sich um einen Typ handelt, der für Sie von Interesse ist:

onSelectedElementChanged: {
    if (selectedElement >= Graphs3D.ElementType.AxisXLabel
            && selectedElement <= Graphs3D.ElementType.AxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

Überprüfen Sie anschließend im „ onPositionChanged “ von inputArea, ob eine Maustaste gedrückt ist und ob eine aktuelle Achsenbeschriftung ausgewählt ist. Sind die Bedingungen erfüllt, rufen Sie die Funktion auf, die die Umwandlung der Mausbewegung in eine Aktualisierung des Achsenbereichs vornimmt:

...
if (pressed && selectedAxisLabel != -1)
    axisDragView.dragAxis();
...

Die Umwandlung ist in diesem Fall einfach, da die Kameradrehung feststeht. Sie können einige vorberechnete Werte verwenden, die zurückgelegte Mausdistanz berechnen und die Werte auf den ausgewählten Achsenbereich anwenden:

function dragAxis() {
    // Do nothing if previous mouse position is uninitialized
    if (previousMouseX === -1)
        return;

    // Directional drag multipliers based on rotation. Camera is locked to 45 degrees, so we
    // can use one precalculated value instead of calculating xx, xy, zx and zy individually
    var cameraMultiplier = 0.70710678;

    // Calculate the mouse move amount
    var moveX = currentMouseX - previousMouseX;
    var moveY = currentMouseY - previousMouseY;

    // Adjust axes
    switch (selectedAxisLabel) {
    case Graphs3D.ElementType.AxisXLabel:
        var distance = ((moveX - moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisX.min -= distance;
            scatterGraph.axisX.max -= distance;
        } else {
            scatterGraph.axisX.max -= distance;
            scatterGraph.axisX.min -= distance;
        }
        break;
    case Graphs3D.ElementType.AxisYLabel:
        distance = moveY / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisY.max += distance;
            scatterGraph.axisY.min += distance;
        } else {
            scatterGraph.axisY.min += distance;
            scatterGraph.axisY.max += distance;
        }
        break;
    case Graphs3D.ElementType.AxisZLabel:
        distance = ((moveX + moveY) * cameraMultiplier) / dragSpeedModifier;
        // Check if we need to change min or max first to avoid invalid ranges
        if (distance > 0) {
            scatterGraph.axisZ.max += distance;
            scatterGraph.axisZ.min += distance;
        } else {
            scatterGraph.axisZ.min += distance;
            scatterGraph.axisZ.max += distance;
        }
        break;
    }
}

Eine komplexere Umwandlung der Mausbewegung in eine Aktualisierung des Achsenbereichs finden Sie in der „Graph Gallery“.

Weitere Funktionen

Das Beispiel veranschaulicht zudem, wie man die orthogonale Projektion einsetzt und wie man die Eigenschaften eines benutzerdefinierten Elements in Echtzeit aktualisiert.

Die orthogonale Projektion ist sehr einfach. Sie müssen lediglich die Eigenschaft „ orthoProjection “ von „ scatterGraph “ ändern. Das Beispiel enthält eine Schaltfläche zum Ein- und Ausschalten dieser Funktion:

Button {
    id: orthoToggle
    width: axisDragView.portraitMode ? parent.width : parent.width / 3
    text: "Display Orthographic"
    anchors.left: axisDragView.portraitMode ? parent.left : rangeToggle.right
    anchors.top: axisDragView.portraitMode ? rangeToggle.bottom : parent.top
    onClicked: {
        if (scatterGraph.orthoProjection) {
            text = "Display Orthographic";
            scatterGraph.orthoProjection = false;
            // Orthographic projection disables shadows, so we need to switch them back on
            scatterGraph.shadowQuality = Graphs3D.ShadowQuality.Medium
        } else {
            text = "Display Perspective";
            scatterGraph.orthoProjection = true;
        }
    }
}

Für benutzerdefinierte Elemente fügen Sie eines zur Eigenschaft „ customItemList “ von „ scatterGraph “ hinzu:

customItemList: [
    Custom3DItem {
        id: qtCube
        meshFile: ":/qml/axishandling/cube.mesh"
        textureFile: ":/qml/axishandling/cubetexture.png"
        position: Qt.vector3d(0.65, 0.35, 0.65)
        scaling: Qt.vector3d(0.3, 0.3, 0.3)
    }
]

Sie implementieren einen Timer zum Hinzufügen, Entfernen und Rotieren aller Elemente im Diagramm und verwenden denselben Timer zum Rotieren des benutzerdefinierten Elements:

onTriggered: {
    rotationAngle = rotationAngle + 1;
    qtCube.setRotationAxisAndAngle(Qt.vector3d(1, 0, 1), rotationAngle);
    ...

Achsenformatierer

Erstellen Sie auf der Registerkarte „ Axis Formatter “ einen benutzerdefinierten Achsenformatierer. Dort wird auch die Verwendung vordefinierter Achsenformatierer veranschaulicht.

Benutzerdefinierter Achsenformatierer

Das Anpassen von Achsenformatierern erfordert die Unterklasse von „ QValue3DAxisFormatter “, was nicht allein mit QML-Code möglich ist. In diesem Beispiel interpretiert die Achse die Float-Werte als Zeitstempel und zeigt das Datum in den Achsenbeschriftungen an. Um dies zu erreichen, führen Sie eine neue Klasse namens „ CustomFormatter “ ein, die eine Unterklasse von „ QValue3DAxisFormatter “ darstellt:

class CustomFormatter : public QValue3DAxisFormatter
{
...

Da Float-Werte eines „ QScatter3DSeries “ aufgrund einer unterschiedlichen Datenbreite nicht direkt in „ QDateTime “-Werte umgewandelt werden können, ist eine Art Zuordnung zwischen den beiden erforderlich. Um diese Zuordnung vorzunehmen, legen Sie ein Ursprungsdatum für den Formatierer fest und interpretieren die Float-Werte aus dem „ QScatter3DSeries “ als Datumsversätze zu diesem Ursprungswert. Das Ursprungsdatum wird als Eigenschaft angegeben:

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

Für die Zuordnung von Werten zu „ QDateTime “ verwenden Sie die Methode „ valueToDateTime() “:

QDateTime CustomFormatter::valueToDateTime(qreal value) const
{
    return m_originDate.startOfDay().addMSecs(qint64(oneDayMs * value));
}

Um als Achsenformatierer zu fungieren, muss ` CustomFormatter ` einige virtuelle Methoden neu implementieren:

QValue3DAxisFormatter *createNewInstance() const override;
void populateCopy(QValue3DAxisFormatter &copy) override;
void recalculate() override;
QString stringForValue(qreal value, const QString &format) override;

Die ersten beiden Methoden sind einfach: Erstellen Sie lediglich eine neue Instanz von ` CustomFormatter ` und kopieren Sie die erforderlichen Daten dorthin. Verwenden Sie diese beiden Methoden, um einen Cache mit Formatierern für die Darstellung zu erstellen und zu aktualisieren. Denken Sie daran, die Implementierung der Oberklasse von ` populateCopy()` aufzurufen:

QValue3DAxisFormatter *CustomFormatter::createNewInstance() const
{
    return new CustomFormatter();
}

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy)
{
    QValue3DAxisFormatter::populateCopy(copy);

    CustomFormatter *customFormatter = static_cast<CustomFormatter *>(&copy);
    customFormatter->m_originDate = m_originDate;
    customFormatter->m_selectionFormat = m_selectionFormat;
}

CustomFormatter Der Großteil der Arbeit wird in der Methode ` recalculate() ` erledigt, in der der Formatierer die Positionen des Rasters, der Unterraster und der Beschriftungen berechnet sowie die Beschriftungszeichenfolgen formatiert. Ignorieren Sie im benutzerdefinierten Formatierer die Segmentanzahl der Achse und zeichnen Sie immer eine Rasterlinie bei Mitternacht. Die Anzahl der Untersegmente und die Positionierung der Beschriftungen werden wie gewohnt behandelt:

void CustomFormatter::recalculate()
{
    // We want our axis to always have gridlines at date breaks

    // Convert range into QDateTimes
    QDateTime minTime = valueToDateTime(qreal(axis()->min()));
    QDateTime maxTime = valueToDateTime(qreal(axis()->max()));

    // Find out the grid counts
    QTime midnight(0, 0);
    QDateTime minFullDate(minTime.date(), midnight);
    int gridCount = 0;
    if (minFullDate != minTime)
        minFullDate = minFullDate.addDays(1);
    QDateTime maxFullDate(maxTime.date(), midnight);

    gridCount += minFullDate.daysTo(maxFullDate) + 1;
    int subGridCount = axis()->subSegmentCount() - 1;

    QList<float> gridPositions;
    QList<float> subGridPositions;
    QList<float> labelPositions;
    QStringList labelStrings;

    // Reserve space for position arrays and label strings
    gridPositions.resize(gridCount);
    subGridPositions.resize((gridCount + 1) * subGridCount);
    labelPositions.resize(gridCount);
    labelStrings.reserve(gridCount);

    // Calculate positions and format labels
    qint64 startMs = minTime.toMSecsSinceEpoch();
    qint64 endMs = maxTime.toMSecsSinceEpoch();
    qreal dateNormalizer = endMs - startMs;
    qreal firstLineOffset = (minFullDate.toMSecsSinceEpoch() - startMs) / dateNormalizer;
    qreal segmentStep = oneDayMs / dateNormalizer;
    qreal subSegmentStep = 0;
    if (subGridCount > 0)
        subSegmentStep = segmentStep / qreal(subGridCount + 1);

    for (int i = 0; i < gridCount; i++) {
        qreal gridValue = firstLineOffset + (segmentStep * qreal(i));
        gridPositions[i] = float(gridValue);
        labelPositions[i] = float(gridValue);
        labelStrings << minFullDate.addDays(i).toString(axis()->labelFormat());
    }

    for (int i = 0; i <= gridCount; i++) {
        if (subGridPositions.size()) {
            for (int j = 0; j < subGridCount; j++) {
                float position;
                if (i)
                    position = gridPositions.at(i - 1) + subSegmentStep * (j + 1);
                else
                    position = gridPositions.at(0) - segmentStep + subSegmentStep * (j + 1);
                if (position > 1.0f || position < 0.0f)
                    position = gridPositions.at(0);
                subGridPositions[i * subGridCount + j] = position;
            }
        }
    }
    setGridPoitions(gridPositions);
    setSubGridPositions(subGridPositions);
    setlabelPositions(labelPositions);
    setLabelStrings(labelStrings);
}

Die Achsenbeschriftungen werden so formatiert, dass nur das Datum angezeigt wird. Um jedoch die Auflösung des Zeitstempels der Auswahlbeschriftung zu erhöhen, geben Sie eine weitere Eigenschaft für den benutzerdefinierten Formatierer an, damit der Benutzer diese anpassen kann:

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY
               selectionFormatChanged)

Diese Eigenschaft des Auswahlformats wird in der neu implementierten Methode „ stringToValue “ verwendet, bei der das übergebene Format ignoriert und durch das benutzerdefinierte Auswahlformat ersetzt wird:

QString CustomFormatter::stringForValue(qreal value, const QString &format)
{
    Q_UNUSED(format);

    return valueToDateTime(value).toString(m_selectionFormat);
}

Um unseren neuen benutzerdefinierten Formatter für QML verfügbar zu machen, deklarieren Sie ihn und machen Sie ihn zu einem QML-Modul. Informationen dazu finden Sie unter „Surface Graph Gallery“.

QML

Definieren Sie im QML-Code für jede Dimension eine eigene Achse:

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

Die Z-Achse ist einfach ein reguläres ` Value3DAxis`:

Value3DAxis {
    id: valueAxis
    segmentCount: 5
    subSegmentCount: 2
    labelFormat: "%.2f"
    min: 0
    max: 10
}

Definieren Sie für die Y-Achse eine logarithmische Achse. Damit „ Value3DAxis “ eine logarithmische Skala anzeigt, geben Sie für die Eigenschaft „ formatter “ der Achse „ LogValue3DAxisFormatter “ an:

Value3DAxis {
    id: logAxis
    formatter: LogValue3DAxisFormatter {
        id: logAxisFormatter
        base: 10
        autoSubGrid: true
        edgeLabelsVisible: true
    }
    labelFormat: "%.2f"
}

Und schließlich verwenden Sie für die X-Achse das neue „ CustomFormatter “:

Value3DAxis {
    id: dateAxis
    formatter: CustomFormatter {
        originDate: "2023-01-01"
        selectionFormat: "yyyy-MM-dd HH:mm:ss"
    }
    subSegmentCount: 2
    labelFormat: "yyyy-MM-dd"
    min: 0
    max: 14
}

Der Rest der Anwendung besteht aus einer weitgehend selbsterklärenden Logik zum Anpassen der Achsen und zum Anzeigen des Diagramms.

Inhalt des Beispiels

Beispielprojekt @ code.qt.io

© 2026 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.