Auf dieser Seite

Achsensteuerung

Implementierung des Ziehens von Achsen mit einem benutzerdefinierten Eingabehandler in QML und Erstellung eines benutzerdefinierten Achsenformatierers.

„Axis Handling“ demonstriert zwei verschiedene benutzerdefinierte Funktionen im Zusammenhang mit 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 Grundfunktionen – eine ausführlichere Dokumentation mit QML-Beispielen finden Sie unter „Einfaches Streudiagramm“.

3D-Streudiagramm mit orangefarbenen Punkten und verschiebbaren gelben Achsenbeschriftungen

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, legen Sie den aktiven Eingabehandler des Scatter3D-Diagramms auf „ null “ fest:

Scatter3D {
    id: scatterGraph
    inputHandler: null
    ...

Fügen Sie anschließend einen „ MouseArea “ hinzu und legen Sie fest, dass er das übergeordnete Element ausfüllt – dies ist derselbe „ 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.scene.selectionQueryPosition = 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 eingeführt wird:

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

Hören Sie in „ scatterGraph “ auf das Signal „ onSelectedElementChanged “. Das Signal wird ausgelöst, nachdem die Auswahlabfrage im „ onPressed “ des „ 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 >= AbstractGraph3D.ElementAxisXLabel
            && selectedElement <= AbstractGraph3D.ElementAxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

Überprüfen Sie anschließend im „ onPositionChanged “ von inputArea, ob eine Maustaste gedrückt ist und ob eine aktuelle Achsenbezeichnungsauswahl vorliegt. 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 Mausbewegungsdistanz 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 AbstractGraph3D.ElementAxisXLabel:
        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 AbstractGraph3D.ElementAxisYLabel:
        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 AbstractGraph3D.ElementAxisZLabel:
        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 Grafikgalerie.

Weitere Funktionen

Das Beispiel veranschaulicht außerdem, wie man die orthogonale Projektion einsetzt und wie man die Eigenschaften eines benutzerdefinierten Elements im laufenden Betrieb 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 = AbstractGraph3D.ShadowQualityMedium
        } 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/qmlaxishandling/cube.obj"
        textureFile: ":/qml/qmlaxishandling/cubetexture.png"
        position: Qt.vector3d(0.65, 0.35, 0.65)
        scaling: Qt.vector3d(0.3, 0.3, 0.3)
    }
]

Sie implementieren einen Timer, um alle Elemente im Diagramm hinzuzufügen, zu entfernen und zu rotieren, und verwenden denselben Timer für die Rotation 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 Erstellung einer 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 von der Klasse „QValue3DAxisFormatter“ abgeleitet ist:

class CustomFormatter : public QValue3DAxisFormatter
{
...

Da Float-Werte einer `QScatter3DSeries` aufgrund unterschiedlicher 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 der `QScatter3DSeries` als Datumsversätze zu diesem Ursprungswert. Das Startdatum 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:

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

Die ersten beiden sind einfach: Erstellen Sie einfach eine neue Instanz von ` CustomFormatter ` und kopieren Sie die erforderlichen Daten dorthin. Verwenden Sie diese beiden Methoden, um einen Cache des Formatierers für Darstellungszwecke zu erstellen und zu aktualisieren. Denken Sie daran, die Superklassen-Implementierung von ` populateCopy()` aufzurufen:

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

void CustomFormatter::populateCopy(QValue3DAxisFormatter &copy) const
{
    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 unser Formatierer die Positionen des Gitters, der Untergitter und der Beschriftungen berechnet sowie die Beschriftungszeichenfolgen formatiert. Ignorieren Sie im benutzerdefinierten Formatierer die Segmentanzahl der Achse und zeichnen Sie immer eine Gitterlinie 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;

    // 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;
            }
        }
    }
}

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

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY selectionFormatChanged)

Diese Eigenschaft für das Auswahlformat wird in der neu implementierten Methode ` stringToValue ` verwendet, wo das übergebene Format ignoriert und durch das benutzerdefinierte Auswahlformat ersetzt wird:

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

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

Um unseren neuen benutzerdefinierten Formatierer für QML verfügbar zu machen, müssen Sie ihn deklarieren und als QML-Modul definieren. 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 eine normale `ValueAxis3D`:

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

Für die Y-Achse definieren Sie eine logarithmische Achse. Damit „ValueAxis3D“ eine logarithmische Skala anzeigt, geben Sie „LogValueAxis3DFormatter“ für die Eigenschaft „ formatter “ der Achse an:

ValueAxis3D {
    id: logAxis
    formatter: LogValueAxis3DFormatter {
        id: logAxisFormatter
        base: 10
        autoSubGrid: true
        showEdgeLabels: true
    }
    labelFormat: "%.2f"
}

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

ValueAxis3D {
    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 recht 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.