Sur cette page

Gestion des axes

Mise en œuvre du glissement d'axe à l'aide d'un gestionnaire d'entrée personnalisé en QML et création d'un formateur d'axe personnalisé.

La section «Gestion des axes » présente deux fonctionnalités personnalisées différentes liées aux axes. Ces fonctionnalités disposent de leurs propres onglets dans l'application.

Les sections suivantes se concentrent uniquement sur ces fonctionnalités et ne traitent pas des fonctionnalités de base. Pour une documentation plus détaillée avec des exemples en QML, consultez la section « Graphique de dispersion simple ».

Nuage de points en 3D avec des sphères et des axes basés sur la date

Exécution de l'exemple

Vous pouvez exécuter l’exemple à partir de :

Glissement des axes

Dans l’onglet « Axis Dragging », implémentez un gestionnaire d’entrée personnalisé en QML qui vous permette de faire glisser les étiquettes d’axe pour modifier les plages des axes. Utilisez ensuite la projection orthographique et mettez à jour dynamiquement les propriétés d’un élément personnalisé.

Remplacement de la gestion des entrées par défaut

Pour désactiver le mécanisme de gestion des entrées par défaut, désactivez le gestionnaire d'entrée par défaut de l'Scatter3D:

unsetDefaultInputHandler();
    ...

Ajoutez ensuite un « MouseArea » et configurez-le pour qu’il occupe tout l’espace de son parent, qui est la mê Item e dans laquelle se trouve notre « scatterGraph ». Configurez-le également pour qu’il n’accepte que les clics du bouton gauche de la souris, car dans cet exemple, les autres boutons ne sont pas nécessaires :

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

Ensuite, écoutez les clics de souris et, lorsqu’un clic est détecté, envoyez une requête de sélection au graphe :

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

Le gestionnaire de signal « onPositionChanged » détecte la position actuelle de la souris, qui sera nécessaire pour le calcul de la distance de déplacement :

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

À la fin de ` onPositionChanged`, enregistrez la position précédente de la souris pour le calcul de la distance de déplacement qui sera présenté plus tard :

...
    previousMouseX = currentMouseX;
    previousMouseY = currentMouseY;
}
Conversion du mouvement de la souris en variation de la plage des axes

Dans ` scatterGraph`, écoutez le signal ` onSelectedElementChanged`. Ce signal est émis après l’envoi de la requête de sélection dans la méthode ` onPressed ` de ` inputArea`. Définissez le type de l’élément dans une propriété que vous avez définie (property int selectedAxisLabel: -1) dans le composant principal, puisqu’il s’agit d’un type qui vous intéresse :

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

Ensuite, de retour dans la méthode onPositionChanged de inputArea, vérifiez si un bouton de la souris est enfoncé et si une sélection d’étiquette d’axe est en cours. Si les conditions sont remplies, appelez la fonction qui effectue la conversion du mouvement de la souris en mise à jour de la plage de l’axe :

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

La conversion est simple dans ce cas, car la rotation de la caméra est fixe. Vous pouvez utiliser des valeurs précalculées, calculer la distance parcourue par la souris et appliquer ces valeurs à la plage de l'axe sélectionné :

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

Pour une conversion plus sophistiquée du mouvement de la souris en mise à jour de la plage de l’axe, consultez la Galerie de graphiques.

Autres fonctionnalités

L'exemple montre également comment utiliser la projection orthographique et comment mettre à jour les propriétés d'un élément personnalisé à la volée.

La projection orthographique est très simple. Il vous suffit de modifier la propriété « orthoProjection » de « scatterGraph ». L’exemple comporte un bouton permettant de l’activer ou de la désactiver :

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

Pour les éléments personnalisés, ajoutez-en un à la propriété ` customItemList ` de ` scatterGraph` :

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

Vous implémentez un minuteur pour ajouter, supprimer et faire défiler tous les éléments du graphique, et vous utilisez ce même minuteur pour faire défiler l’élément personnalisé :

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

Formateurs d’axes

Dans l’onglet « Axis Formatter », créez un formateur d’axe personnalisé. Cet exemple illustre également comment utiliser des formateurs d’axe prédéfinis.

Formateur d’axe personnalisé

La personnalisation des formateurs d’axe nécessite de créer une sous-classe de ` QValue3DAxisFormatter`, ce qui ne peut pas être réalisé uniquement avec du code QML. Dans cet exemple, l’axe interprète les valeurs de type float comme des horodatages et affiche la date dans les étiquettes d’axe. Pour ce faire, créez une nouvelle classe appelée ` CustomFormatter`, qui est une sous-classe de ` QValue3DAxisFormatter` :

class CustomFormatter : public QValue3DAxisFormatter
{
...

Étant donné que les valeurs de type float d’un « QScatter3DSeries » ne peuvent pas être directement converties en valeurs de type « QDateTime » en raison d’une différence de largeur de données, un mappage entre les deux est nécessaire. Pour effectuer ce mappage, spécifiez une date d’origine pour le formateur et interprétez les valeurs de type float provenant de « QScatter3DSeries » comme des décalages de date par rapport à cette valeur d’origine. La date d’origine est fournie sous forme de propriété :

Q_PROPERTY(QDate originDate READ originDate WRITE setOriginDate NOTIFY originDateChanged)

Pour la mise en correspondance entre une valeur et une date « QDateTime », utilisez la méthode valueToDateTime():

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

Pour fonctionner en tant que formateur d’axe, ` CustomFormatter ` doit réimplémenter certaines méthodes virtuelles :

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

Les deux premières méthodes sont simples : il suffit de créer une nouvelle instance de ` CustomFormatter ` et d’y copier les données nécessaires. Utilisez ces deux méthodes pour créer et mettre à jour un cache de formateurs à des fins de rendu. N’oubliez pas d’appeler l’implémentation de la superclasse de ` populateCopy()` :

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 effectue l’essentiel de son travail dans la méthode ` recalculate() `, où le formateur calcule les positions de la grille, des sous-grilles et des étiquettes, et met en forme les chaînes des étiquettes. Dans le formateur personnalisé, ignorez le nombre de segments de l’axe et tracez toujours une ligne de grille à minuit. Le nombre de sous-segments et le positionnement des étiquettes sont gérés normalement :

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

Les étiquettes d'axe sont formatées pour n'afficher que la date. Cependant, pour augmenter la résolution de l'horodatage de l'étiquette de sélection, spécifiez une autre propriété pour le formateur personnalisé afin de permettre à l'utilisateur de la personnaliser :

Q_PROPERTY(QString selectionFormat READ selectionFormat WRITE setSelectionFormat NOTIFY
               selectionFormatChanged)

Cette propriété de format de sélection est utilisée dans la méthode ` stringToValue ` réimplémentée, où le format fourni est ignoré et remplacé par le format de sélection personnalisé :

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

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

Pour exposer notre nouveau formateur personnalisé au QML, déclarez-le et transformez-le en module QML. Pour plus d’informations sur la marche à suivre, consultez la section « Surface Graph Gallery ».

QML

Dans le code QML, définissez un axe différent pour chaque dimension :

axisZ: valueAxis
axisY: logAxis
axisX: dateAxis

L'axe Z correspond simplement à une échelle linéaire standard Value3DAxis:

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

Pour l’axe Y, définissez un axe logarithmique. Pour que l’ Value3DAxis affiche une échelle logarithmique, spécifiez LogValue3DAxisFormatter pour la propriété « formatter » de l’axe :

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

Et enfin, pour l’axe X, utilisez le nouvel 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
}

Le reste de l’application consiste en une logique assez intuitive permettant de modifier les axes et d’afficher le graphique.

Exemple de contenu

Projet d'exemple sur 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.