Achsenbehandlung

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

Axis Handling demonstriert zwei verschiedene benutzerdefinierte Funktionen mit Achsen. Die Funktionen haben ihre eigenen Registerkarten in der Anwendung.

Die folgenden Abschnitte konzentrieren sich nur auf diese Funktionen und überspringen die Erklärung der grundlegenden Funktionalität - eine ausführlichere QML-Beispieldokumentation finden Sie unter Einfaches Streudiagramm.

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.

Ziehen der Achsen

Implementieren Sie auf der Registerkarte Axis Dragging einen benutzerdefinierten Input-Handler in QML, mit dem Sie Achsenbeschriftungen ziehen können, um Achsenbereiche zu ändern. Außerdem können Sie die orthografische Projektion verwenden und die Eigenschaften eines benutzerdefinierten Elements dynamisch aktualisieren.

Außerkraftsetzen der Standard-Eingabebehandlung

Um den Standard-Eingabehandlungsmechanismus zu deaktivieren, setzen Sie den aktiven Input-Handler von Scatter3D graph auf null:

Scatter3D {
    id: scatterGraph
    inputHandler: null
    ...

Fügen Sie dann ein MouseArea hinzu und stellen Sie es so ein, dass es das übergeordnete Element füllt, also dasselbe Item, in dem unser scatterGraph enthalten ist. Legen Sie außerdem fest, dass nur das Drücken der linken Maustaste akzeptiert wird, da in diesem Beispiel die anderen Tasten nicht benötigt werden:

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

Achten Sie dann auf das Drücken der Maustaste und senden Sie eine Auswahlabfrage an den Graphen, wenn diese abgefangen wird:

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

Der onPositionChanged signal handler fängt die aktuelle Mausposition ab, die für die Berechnung der Bewegungsdistanz benötigt wird:

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

Am Ende von onPositionChanged speichern Sie die vorherige Mausposition für die Berechnung des Verschiebeabstands, die später eingeführt wird:

...
previousMouseX = currentMouseX;
previousMouseY = currentMouseY;
}
Übersetzung der Mausbewegung in eine Achsenbereichsänderung

Hören Sie in scatterGraph auf onSelectedElementChanged. Das Signal wird nach der Selektionsabfrage in onPressed von inputArea ausgegeben. Setzen Sie den Elementtyp in eine Eigenschaft, die Sie in der Hauptkomponente definiert haben (property int selectedAxisLabel: -1), da es sich um einen Typ handelt, an dem Sie interessiert sind:

onSelectedElementChanged: {
    if (selectedElement >= AbstractGraph3D.ElementAxisXLabel
            && selectedElement <= AbstractGraph3D.ElementAxisZLabel) {
        selectedAxisLabel = selectedElement;
    } else {
        selectedAxisLabel = -1;
    }
}

Prüfen Sie dann, zurück in onPositionChanged von inputArea, ob eine Maustaste gedrückt wird und ob Sie eine aktuelle Achsenbeschriftung ausgewählt haben. Wenn die Bedingungen erfüllt sind, rufen Sie die Funktion auf, die die Umwandlung von der Mausbewegung zur Aktualisierung des Achsenbereichs vornimmt:

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

Die Konvertierung ist in diesem Fall einfach, da die Kameradrehung festgelegt ist. Sie können einige vorberechnete Werte verwenden, den Mausbewegungsabstand 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;
    }
}

Für eine anspruchsvollere Konvertierung von Mausbewegung zu Achsenbereichsaktualisierung siehe Graph Gallery.

Andere Funktionen

Das Beispiel veranschaulicht auch die Verwendung der orthografischen Projektion und die Aktualisierung der Eigenschaften eines benutzerdefinierten Elements im laufenden Betrieb.

Die orthografische Projektion ist sehr einfach. Sie müssen lediglich die Eigenschaft orthoProjection von scatterGraph ändern. Das Beispiel verfügt über eine Schaltfläche zum Ein- und Ausschalten der Projektion:

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 eine zu 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)
    }
]

Implementieren Sie einen Zeitgeber, um alle Elemente im Diagramm hinzuzufügen, zu entfernen und zu drehen, und verwenden Sie denselben Zeitgeber für das Drehen des benutzerdefinierten Elements:

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

Achsenformatierer

Auf der Registerkarte Axis Formatter erstellen Sie einen benutzerdefinierten Achsenformatierer. Hier wird auch gezeigt, wie Sie vordefinierte Achsenformatierer verwenden können.

Benutzerdefinierte Achsenformatierer

Das Anpassen von Achsenformatierern erfordert die Unterklassifizierung von QValue3DAxisFormatter, was nicht allein im 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 ist:

class CustomFormatter : public QValue3DAxisFormatter
{
...

Da Float-Werte einer QScatter3DSeries aufgrund der unterschiedlichen Datenbreite nicht direkt in QDateTime Werte umgewandelt werden können, ist eine Art von Mapping zwischen den beiden erforderlich. Um die Zuordnung vorzunehmen, geben Sie ein Ursprungsdatum für den Formatierer an und interpretieren die Float-Werte von QScatter3DSeries als Datumsoffsets zu diesem Ursprungswert. Das Ursprungsdatum wird als Eigenschaft angegeben:

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

Für das Mapping von Wert 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 funktionieren, muss CustomFormatter einige virtuelle Methoden reimplementieren:

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 zum Erstellen und Aktualisieren eines Cache von Formatter für Rendering-Zwecke. 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 Die Methode recalculate(), in der unser Formatierer die Positionen des Gitters, der Untergitter und der Beschriftungen berechnet und die Beschriftungszeichenfolgen formatiert, leistet den größten Teil seiner Arbeit. Im benutzerdefinierten Formatierer ignorieren Sie die Segmentanzahl der Achse und zeichnen eine Rasterlinie immer um Mitternacht. Die Anzahl der Untersegmente und die Positionierung der Beschriftungen werden normal 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 sind so formatiert, dass sie nur das Datum anzeigen. 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 ihn 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) const
{
    Q_UNUSED(format);

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

Um unseren neuen benutzerdefinierten Formatierer in QML freizugeben, deklarieren Sie ihn und machen Sie ihn zu einem QML-Modul. Informationen dazu finden Sie in der Surface Graph Gallery.

QML

Definieren Sie im QML-Code für jede Dimension eine andere 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 die 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 ziemlich selbsterklärenden Logik für die Änderung der Achsen und die Darstellung des Diagramms.

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.