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 ©) 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 ©) const { QValue3DAxisFormatter::populateCopy(copy); CustomFormatter *customFormatter = static_cast<CustomFormatter *>(©); 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
© 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.