Sur cette page

Galerie de graphiques

Galerie de graphiques en barres, en nuage de points et en surface.

Lagalerie de graphiques présente les trois types de graphiques et certaines de leurs caractéristiques particulières. Les graphiques ont leurs propres onglets dans l'application.

Exécution de l'exemple

Pour exécuter l'exemple à partir de Qt Creatorouvrez le mode Welcome et sélectionnez l'exemple à partir de Examples. Pour plus d'informations, voir Qt Creator: Tutorial : Construire et exécuter.

Graphique à barres

Dans l'onglet Bar Graph, créez un graphique à barres en 3D à l'aide de Q3DBars et combinez l'utilisation de widgets pour ajuster les différentes qualités du graphique à barres. L'exemple montre comment :

  • Créer une application avec Q3DBars et quelques widgets
  • utiliser QBar3DSeries et QBarDataProxy pour définir les données du graphique
  • Ajuster certaines propriétés du graphique et de la série à l'aide des contrôles des widgets
  • Sélectionner une ligne ou une colonne en cliquant sur l'étiquette d'un axe
  • Créer un proxy personnalisé à utiliser avec Q3DBars

Pour plus d'informations sur l'interaction avec le graphique, voir cette page.

Création de l'application

Tout d'abord, dans bargraph.cpp, instanciez Q3DBars:

m_barsGraph = new Q3DBars();

Ensuite, créez le widget, ainsi que les dispositions horizontale et verticale.

Le graphique est intégré dans un conteneur de fenêtre à l'aide de QWidget::createWindowContainer(). Cela est nécessaire car toutes les classes de graphiques de visualisation de données (Q3DBars, Q3DScatter, Q3DSurface) héritent de QWindow. C'est la seule façon d'utiliser une classe héritant de QWindow en tant que widget.

Ajoutez le graphique et la disposition verticale à la disposition horizontale :

m_barsWidget = new QWidget;
auto *hLayout = new QHBoxLayout(m_barsWidget);
m_container = QWidget::createWindowContainer(m_barsGraph, m_barsWidget);
m_barsGraph->resize(minimumGraphSize);
m_container->setMinimumSize(minimumGraphSize);
m_container->setMaximumSize(maximumGraphSize);
m_container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
m_container->setFocusPolicy(Qt::StrongFocus);
hLayout->addWidget(m_container, 1);

auto *vLayout = new QVBoxLayout();
hLayout->addLayout(vLayout);

Créez ensuite une autre classe pour gérer l'ajout de données et d'autres interactions avec le graphique :

auto *modifier = new GraphModifier(m_barsGraph, this);
Mise en place du graphique à barres

Configurez le graphique dans le constructeur de la classe GraphModifier:

GraphModifier::GraphModifier(Q3DBars *bargraph, QObject *parent) :
      QObject(parent),
      m_graph(bargraph),

Tout d'abord, créez les axes et les séries dans des variables membres afin de pouvoir les modifier facilement :

m_temperatureAxis(new QValue3DAxis),
m_yearAxis(new QCategory3DAxis),
m_monthAxis(new QCategory3DAxis),
m_primarySeries(new QBar3DSeries),
m_secondarySeries(new QBar3DSeries),
m_celsiusString(u"°C"_s)

Ensuite, définissez certaines qualités visuelles pour le graphique :

m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftMedium);
m_graph->activeTheme()->setBackgroundEnabled(false);
m_graph->activeTheme()->setFont(QFont("Times New Roman", m_fontSize));
m_graph->activeTheme()->setLabelBackgroundEnabled(true);
m_graph->setMultiSeriesUniform(true);

Configurez les axes et faites-en les axes actifs du graphique :

m_temperatureAxis->setTitle("Average temperature");
m_temperatureAxis->setSegmentCount(m_segments);
m_temperatureAxis->setSubSegmentCount(m_subSegments);
m_temperatureAxis->setRange(m_minval, m_maxval);
m_temperatureAxis->setLabelFormat(u"%.1f "_s + m_celsiusString);
m_temperatureAxis->setLabelAutoRotation(30.0f);
m_temperatureAxis->setTitleVisible(true);

m_yearAxis->setTitle("Year");
m_yearAxis->setLabelAutoRotation(30.0f);
m_yearAxis->setTitleVisible(true);
m_monthAxis->setTitle("Month");
m_monthAxis->setLabelAutoRotation(30.0f);
m_monthAxis->setTitleVisible(true);

m_graph->setValueAxis(m_temperatureAxis);
m_graph->setRowAxis(m_yearAxis);
m_graph->setColumnAxis(m_monthAxis);

Donnez aux étiquettes des axes un petit angle d'autorotation avec setLabelAutoRotation() pour qu'elles s'orientent légèrement vers la caméra. Cela permet d'améliorer la lisibilité des étiquettes des axes dans les angles extrêmes de la caméra.

Ensuite, initialisez les propriétés visuelles des séries. Notez que la deuxième série n'est pas visible au départ :

m_primarySeries->setItemLabelFormat(u"Oulu - @colLabel @rowLabel: @valueLabel"_s);
m_primarySeries->setMesh(QAbstract3DSeries::MeshBevelBar);
m_primarySeries->setMeshSmooth(false);

m_secondarySeries->setItemLabelFormat(u"Helsinki - @colLabel @rowLabel: @valueLabel"_s);
m_secondarySeries->setMesh(QAbstract3DSeries::MeshBevelBar);
m_secondarySeries->setMeshSmooth(false);
m_secondarySeries->setVisible(false);

Ajoutez la série au graphique :

m_graph->addSeries(m_primarySeries);
m_graph->addSeries(m_secondarySeries);

Enfin, définissez l'angle de la caméra en appelant la même méthode que celle utilisée par le bouton de changement d'angle de la caméra dans l'interface utilisateur pour passer d'un angle de caméra à l'autre :

changePresetCamera();

La caméra est contrôlée par l'objet scène du graphique :

static int preset = Q3DCamera::CameraPresetFront;

m_graph->scene()->activeCamera()->setCameraPreset((Q3DCamera::CameraPreset)preset);

if (++preset > Q3DCamera::CameraPresetDirectlyBelow)
    preset = Q3DCamera::CameraPresetFrontLow;

Pour plus d'informations sur l'utilisation des scènes et des caméras, voir Q3DScene et Q3DCamera.

Ajout de données au graphique

À la fin du constructeur, appelez une méthode qui configure les données :

resetTemperatureData();

Cette méthode ajoute des données aux proxies des deux séries :

// Set up data
static const float tempOulu[8][12] = {
    {-7.4f, -2.4f, 0.0f, 3.0f, 8.2f, 11.6f, 14.7f, 15.4f, 11.4f, 4.2f, 2.1f, -2.3f},       // 2015
    {-13.4f, -3.9f, -1.8f, 3.1f, 10.6f, 13.7f, 17.8f, 13.6f, 10.7f, 3.5f, -3.1f, -4.2f},   // 2016
...
auto *dataSet = new QBarDataArray;
auto *dataSet2 = new QBarDataArray;

dataSet->reserve(m_years.size());
for (qsizetype year = 0; year < m_years.size(); ++year) {
    // Create a data row
    auto *dataRow = new QBarDataRow(m_months.size());
    auto *dataRow2 = new QBarDataRow(m_months.size());
    for (qsizetype month = 0; month < m_months.size(); ++month) {
        // Add data to the row
        (*dataRow)[month].setValue(tempOulu[year][month]);
        (*dataRow2)[month].setValue(tempHelsinki[year][month]);
    }
    // Add the row to the set
    dataSet->append(dataRow);
    dataSet2->append(dataRow2);
}

// Add data to the data proxy (the data proxy assumes ownership of it)
m_primarySeries->dataProxy()->resetArray(dataSet, m_years, m_months);
m_secondarySeries->dataProxy()->resetArray(dataSet2, m_years, m_months);
Utiliser des widgets pour contrôler le graphique

Poursuivez en ajoutant quelques widgets à l'adresse bargraph.cpp. Ajoutez un curseur :

auto *rotationSliderX = new QSlider(Qt::Horizontal, m_barsWidget);
rotationSliderX->setTickInterval(30);
rotationSliderX->setTickPosition(QSlider::TicksBelow);
rotationSliderX->setMinimum(-180);
rotationSliderX->setValue(0);
rotationSliderX->setMaximum(180);

Utilisez le curseur pour faire pivoter le graphique au lieu d'utiliser simplement la souris ou le toucher. Ajoutez-le à la disposition verticale :

vLayout->addWidget(new QLabel(u"Rotate horizontally"_s));
vLayout->addWidget(rotationSliderX, 0, Qt::AlignTop);

Ensuite, reliez-le à une méthode dans GraphModifier:

QObject::connect(rotationSliderX, &QSlider::valueChanged, modifier, &GraphModifier::rotateX);

Créez un emplacement dans GraphModifier pour la connexion du signal. La caméra est contrôlée par l'objet scène. Cette fois, spécifiez la position réelle de la caméra le long de l'orbite autour du point central, au lieu de spécifier un angle de caméra prédéfini :

void GraphModifier::rotateX(int rotation)
{
    m_xRotation = rotation;
    m_graph->scene()->activeCamera()->setCameraPosition(m_xRotation, m_yRotation);
}

Vous pouvez maintenant utiliser le curseur pour faire pivoter le graphique.

Ajoutez d'autres widgets à la disposition verticale pour contrôler :

  • la rotation du graphique
  • Style d'étiquette
  • Préréglage de la caméra
  • Visibilité de l'arrière-plan
  • Visibilité de la grille
  • Lissage de l'ombrage des barres
  • Visibilité de la deuxième série de barres
  • Direction de l'axe des valeurs
  • Visibilité et rotation du titre de l'axe
  • Plage de données à afficher
  • Style des barres
  • Mode de sélection
  • Thème
  • Qualité de l'ombre
  • Police
  • Taille de la police
  • Rotation de l'étiquette de l'axe
  • Mode données

Certains contrôles de widgets sont intentionnellement désactivés lorsqu'ils sont en mode données ( Custom Proxy Data ).

Sélection d'une ligne ou d'une colonne en cliquant sur une étiquette d'axe

La sélection par étiquette d'axe est une fonctionnalité par défaut pour les graphiques à barres. Par exemple, vous pouvez sélectionner des lignes en cliquant sur une étiquette d'axe de la manière suivante :

  1. Changez le mode de sélection en SelectionRow
  2. Cliquez sur l'étiquette d'une année
  3. La ligne correspondant à l'année cliquée est sélectionnée.

La même méthode fonctionne avec les drapeaux SelectionSlice et SelectionItem, à condition que SelectionRow ou SelectionColumn soit également activé.

Zoom sur la sélection

Pour illustrer l'ajustement de la cible de la caméra, implémentez une animation de zoom jusqu'à la sélection via une pression sur un bouton. L'initialisation de l'animation se fait dans le constructeur :

Q3DCamera *camera = m_graph->scene()->activeCamera();
m_defaultAngleX = camera->xRotation();
m_defaultAngleY = camera->yRotation();
m_defaultZoom = camera->zoomLevel();
m_defaultTarget = camera->target();

m_animationCameraX.setTargetObject(camera);
m_animationCameraY.setTargetObject(camera);
m_animationCameraZoom.setTargetObject(camera);
m_animationCameraTarget.setTargetObject(camera);

m_animationCameraX.setPropertyName("xRotation");
m_animationCameraY.setPropertyName("yRotation");
m_animationCameraZoom.setPropertyName("zoomLevel");
m_animationCameraTarget.setPropertyName("target");

int duration = 1700;
m_animationCameraX.setDuration(duration);
m_animationCameraY.setDuration(duration);
m_animationCameraZoom.setDuration(duration);
m_animationCameraTarget.setDuration(duration);

// The zoom always first zooms out above the graph and then zooms in
qreal zoomOutFraction = 0.3;
m_animationCameraX.setKeyValueAt(zoomOutFraction, QVariant::fromValue(0.0f));
m_animationCameraY.setKeyValueAt(zoomOutFraction, QVariant::fromValue(90.0f));
m_animationCameraZoom.setKeyValueAt(zoomOutFraction, QVariant::fromValue(50.0f));
m_animationCameraTarget.setKeyValueAt(zoomOutFraction,
                                      QVariant::fromValue(QVector3D(0.0f, 0.0f, 0.0f)));

La fonction GraphModifier::zoomToSelectedBar() contient la fonctionnalité de zoom. QPropertyAnimation m_animationCameraTarget cible la propriété Q3DCamera::target, qui prend une valeur normalisée dans l'intervalle (-1, 1).

Déterminez la position de la barre sélectionnée par rapport aux axes et utilisez-la comme valeur finale pour m_animationCameraTarget:

QVector3D endTarget;
float xMin = m_graph->columnAxis()->min();
float xRange = m_graph->columnAxis()->max() - xMin;
float zMin = m_graph->rowAxis()->min();
float zRange = m_graph->rowAxis()->max() - zMin;
endTarget.setX((selectedBar.y() - xMin) / xRange * 2.0f - 1.0f);
endTarget.setZ((selectedBar.x() - zMin) / zRange * 2.0f - 1.0f);
...
m_animationCameraTarget.setEndValue(QVariant::fromValue(endTarget));

Ensuite, faites pivoter la caméra de manière à ce qu'elle pointe toujours approximativement vers le centre du graphique à la fin de l'animation :

qreal endAngleX = 90.0 - qRadiansToDegrees(qAtan(qreal(endTarget.z() / endTarget.x())));
if (endTarget.x() > 0.0f)
    endAngleX -= 180.0f;
float barValue = m_graph->selectedSeries()->dataProxy()->itemAt(selectedBar.x(),
                                                                selectedBar.y())->value();
float endAngleY = barValue >= 0.0f ? 30.0f : -30.0f;
if (m_graph->valueAxis()->reversed())
    endAngleY *= -1.0f;
Proxy personnalisé pour les données

En activant le mode de données Custom Proxy Data, un jeu de données personnalisé et le proxy correspondant sont utilisés.

Définissez un ensemble de données flexible simple, VariantDataSet, où chaque élément de données est une liste de variantes. Chaque élément peut avoir plusieurs valeurs, identifiées par leur index dans la liste. Dans ce cas, l'ensemble de données stocke des données mensuelles sur les précipitations, où la valeur de l'index zéro est l'année, la valeur de l'index un est le mois et la valeur de l'index deux est la quantité de précipitations au cours de ce mois.

Le proxy personnalisé est similaire aux proxys basés sur les modèles d'éléments fournis par Qt Data Visualization, et il nécessite un mappage pour interpréter les données.

VariantDataSet

Définir les éléments de données comme des objets QVariantList. Ajoutez une fonctionnalité permettant d'effacer l'ensemble de données et de demander une référence aux données contenues dans l'ensemble. Ajoutez également des signaux à émettre lorsque des données sont ajoutées ou que l'ensemble est effacé :

using VariantDataItem = QVariantList;
using VariantDataItemList = QList<VariantDataItem *>;
...

    void clear();

    int addItem(VariantDataItem *item);
    int addItems(VariantDataItemList *itemList);

    const VariantDataItemList &itemList() const;

Q_SIGNALS:
    void itemsAdded(int index, int count);
    void dataCleared();
VariantBarDataProxy

Sous-classe VariantBarDataProxy de QBarDataProxy et fournit une API simple de getters et setters pour l'ensemble de données et le mapping :

class VariantBarDataProxy : public QBarDataProxy
...

// Doesn't gain ownership of the dataset, but does connect to it to listen for data changes.
void setDataSet(VariantDataSet *newSet);
VariantDataSet *dataSet();

// Map key (row, column, value) to value index in data item (VariantItem).
// Doesn't gain ownership of mapping, but does connect to it to listen for mapping changes.
// Modifying mapping that is set to proxy will trigger dataset re-resolving.
void setMapping(VariantBarDataMapping *mapping);
VariantBarDataMapping *mapping();

Le proxy est à l'écoute des modifications apportées à l'ensemble de données et au mappage, et résout l'ensemble de données si des modifications sont détectées. Il ne s'agit pas d'une implémentation particulièrement efficace, car tout changement entraînera une nouvelle résolution de l'ensemble des données, mais ce n'est pas un problème dans le cadre de cet exemple.

Dans la méthode resolveDataSet(), trier les valeurs des données variantes en lignes et en colonnes sur la base de la mise en correspondance. Cette méthode est très similaire à la façon dont QItemModelBarDataProxy gère le mappage, sauf que vous utilisez ici des index de liste au lieu de rôles de modèle d'élément. Une fois les valeurs triées, générez QBarDataArray à partir d'elles et appelez la méthode resetArray() dans la classe mère :

void VariantBarDataProxy::resolveDataSet()
{
    // If we have no data or mapping, or the categories are not defined, simply clear the array
    if (m_dataSet.isNull() || m_mapping.isNull() || !m_mapping->rowCategories().size()
            || !m_mapping->columnCategories().size()) {
        resetArray(nullptr);
        return;
    }
    const VariantDataItemList &itemList = m_dataSet->itemList();

    int rowIndex = m_mapping->rowIndex();
    int columnIndex = m_mapping->columnIndex();
    int valueIndex = m_mapping->valueIndex();
    const QStringList &rowList = m_mapping->rowCategories();
    const QStringList &columnList = m_mapping->columnCategories();

    // Sort values into rows and columns
    using ColumnValueMap = QHash<QString, float>;
    QHash <QString, ColumnValueMap> itemValueMap;
    for (const VariantDataItem *item : itemList) {
        itemValueMap[item->at(rowIndex).toString()][item->at(columnIndex).toString()]
                = item->at(valueIndex).toReal();
    }

    // Create a new data array in format the parent class understands
    auto *newProxyArray = new QBarDataArray;
    for (const QString &rowKey : rowList) {
        auto *newProxyRow = new QBarDataRow(columnList.size());
        for (qsizetype i = 0; i < columnList.size(); ++i)
            (*newProxyRow)[i].setValue(itemValueMap[rowKey][columnList.at(i)]);
        newProxyArray->append(newProxyRow);
    }

    // Finally, reset the data array in the parent class
    resetArray(newProxyArray);
}
VariantBarDataMapping

Stocker les informations de mappage entre les index des éléments de données de VariantDataSet et les lignes, les colonnes et les valeurs de QBarDataArray dans VariantBarDataMapping. Il contient les listes de lignes et de colonnes à inclure dans les données résolues :

Q_PROPERTY(int rowIndex READ rowIndex WRITE setRowIndex NOTIFY rowIndexChanged)
Q_PROPERTY(int columnIndex READ columnIndex WRITE setColumnIndex NOTIFY columnIndexChanged)
Q_PROPERTY(int valueIndex READ valueIndex WRITE setValueIndex NOTIFY valueIndexChanged)
Q_PROPERTY(QStringList rowCategories READ rowCategories WRITE setRowCategories NOTIFY rowCategoriesChanged)
Q_PROPERTY(QStringList columnCategories READ columnCategories WRITE setColumnCategories NOTIFY columnCategoriesChanged)
...

explicit VariantBarDataMapping(int rowIndex, int columnIndex, int valueIndex,
                               const QStringList &rowCategories,
                               const QStringList &columnCategories);
...

void remap(int rowIndex, int columnIndex, int valueIndex,
           const QStringList &rowCategories,
           const QStringList &columnCategories);
...

void mappingChanged();

La principale façon d'utiliser un objet VariantBarDataMapping est d'indiquer les correspondances dans le constructeur, bien que vous puissiez utiliser la méthode remap() pour les définir ultérieurement, individuellement ou dans leur ensemble. Émettre un signal en cas de changement de mappage. Le résultat est une version simplifiée de la fonctionnalité de mappage de QItemModelBarDataProxy, adaptée pour fonctionner avec des listes de variantes au lieu de modèles d'éléments.

Données sur les précipitations

Gérer la configuration de QBar3DSeries avec le proxy personnalisé dans la classe RainfallData:

m_proxy = new VariantBarDataProxy;
m_series = new QBar3DSeries(m_proxy);

Remplir l'ensemble de données de la variante dans la méthode addDataSet():

void RainfallData::addDataSet() { // Créer un nouvel ensemble de données et une nouvelle liste d'éléments de donnéesm_dataSet = new VariantDataSet ; auto *itemList = new VariantDataItemList ; // Lire les données d'un fichier de données dans la liste d'éléments de données   QFile dataFile(":/data/raindata.txt") ; if (dataFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QTextStream stream(&dataFile) ; while (!stream.atEnd()) { QString line = stream.readLine() ; if (line.startsWith('#')) // Ignore les commentaires continue; const auto strList = QStringView{line}.split(',', Qt::SkipEmptyParts) ; // Chaque ligne contient trois données : Année, mois et valeur des précipitations if (strList.size() < 3) {                qWarning() << "Invalid row read from data:" << line;
               continue; } // Stocker l'année et le mois sous forme de chaînes, et la valeur pluviométrique sous forme de double // dans un élément de données variant et ajouter l'élément à la liste d'éléments.
           auto *newItem = new VariantDataItem ; for(int i = 0; i < 2; ++i)  newItem->append(strList.at(i).trimmed().toString()) ;  newItem->append(strList.at(2).trimmed().toDouble()) ;  itemList->append(newItem) ; } } else {        qWarning() << "Unable to open data file:" << dataFile.fileName();
    } ...

Ajoutez l'ensemble de données au proxy personnalisé et définissez le mappage :

// Add items to the data set and set it to the proxy
m_dataSet->addItems(itemList);
m_proxy->setDataSet(m_dataSet);

// Create new mapping for the data and set it to the proxy
m_mapping = new VariantBarDataMapping(0, 1, 2, m_years, m_numericMonths);
m_proxy->setMapping(m_mapping);

Enfin, ajoutez une fonction qui récupère la série créée pour l'afficher :

QBar3DSeries *customSeries() { return m_series; }

Graphique de dispersion

Dans l'onglet Scatter Graph, créez un graphique de dispersion en 3D à l'aide de Q3DScatter. L'exemple montre comment :

Pour la création d'une application de base, voir Bar Graph.

Configuration du graphique en nuage de points

Commencez par définir certaines qualités visuelles du graphique dans le constructeur de l'application ScatterDataModifier:

m_graph->activeTheme()->setType(Q3DTheme::ThemeStoneMoss);
m_graph->setShadowQuality(QAbstract3DGraph::ShadowQualitySoftHigh);
m_graph->scene()->activeCamera()->setCameraPreset(Q3DCamera::CameraPresetFront);
m_graph->scene()->activeCamera()->setZoomLevel(80.f);

Aucun de ces éléments n'est obligatoire, mais ils sont utilisés pour remplacer les valeurs par défaut du graphique. Vous pouvez essayer de voir ce que cela donne avec les valeurs par défaut prédéfinies en commentant le bloc ci-dessus.

Ensuite, créez un QScatterDataProxy et le QScatter3DSeries associé. Définissez un format d'étiquette personnalisé et un lissage de maillage pour la série et ajoutez-la au graphique :

auto *proxy = new QScatterDataProxy;
auto *series = new QScatter3DSeries(proxy);
series->setItemLabelFormat(u"@xTitle: @xLabel @yTitle: @yLabel @zTitle: @zLabel"_s);
series->setMeshSmooth(m_smooth);
m_graph->addSeries(series);
Ajout de données de dispersion

La dernière chose à faire dans le constructeur de ScatterDataModifier est d'ajouter des données au graphique :

addData();

L'ajout des données est effectué dans la méthode addData(). Commencez par configurer les axes :

m_graph->axisX()->setTitle("X");
m_graph->axisY()->setTitle("Y");
m_graph->axisZ()->setTitle("Z");

Vous pouvez également le faire dans le constructeur de ScatterDataModifier. En le faisant ici, le constructeur reste plus simple et la configuration des axes est proche des données.

Ensuite, créez un tableau de données et remplissez-le :

auto *dataArray = new QScatterDataArray;
dataArray->reserve(m_itemCount);
    ...
const float limit = qSqrt(m_itemCount) / 2.0f;
for (int i = -limit; i < limit; ++i) {
    for (int j = -limit; j < limit; ++j) {
        const float x = float(i) + 0.5f;
        const float y = qCos(qDegreesToRadians(float(i * j) / m_curveDivider));
        const float z = float(j) + 0.5f;
        dataArray->append(QScatterDataItem({x, y, z}));
    }
}

Enfin, dites au proxy de commencer à utiliser les données que nous lui avons fournies :

m_graph->seriesList().at(0)->dataProxy()->resetArray(dataArray);

Le graphique dispose désormais des données et est prêt à être utilisé. Pour plus d'informations sur l'ajout de widgets pour contrôler le graphique, voir Utiliser des widgets pour contrôler le graphique.

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

Initialiser m_inputHandler dans le constructeur avec un pointeur sur l'instance de graphique de dispersion :

m_inputHandler(new AxesInputHandler(scatter))

Remplacer le mécanisme de gestion des entrées par défaut en définissant le gestionnaire d'entrée actif de Q3DScatter à AxesInputHandler, qui met en œuvre le comportement personnalisé :

// Give ownership of the handler to the graph and make it the active handler
m_graph->setActiveInputHandler(m_inputHandler);

Le gestionnaire d'entrée a besoin d'accéder aux axes du graphique, il faut donc les lui transmettre :

// Give our axes to the input handler
m_inputHandler->setAxes(m_graph->axisX(), m_graph->axisZ(), m_graph->axisY());
Extension de la gestion des événements de la souris

Tout d'abord, héritez le gestionnaire d'entrée personnalisé de Q3DInputHandler au lieu de QAbstract3DInputHandler afin de conserver toutes les fonctionnalités de la gestion d'entrée par défaut et d'ajouter la fonctionnalité personnalisée par-dessus :

class AxesInputHandler : public Q3DInputHandler

Commencez à étendre la fonctionnalité par défaut en réimplémentant certains événements de la souris. Commencez par étendre mousePressEvent. Ajoutez-y un drapeau m_mousePressed pour le bouton gauche de la souris, et conservez le reste de la fonctionnalité par défaut :

void AxesInputHandler::mousePressEvent(QMouseEvent *event, const QPoint &mousePos)
{
    Q3DInputHandler::mousePressEvent(event, mousePos);
    if (Qt::LeftButton == event->button())
        m_mousePressed = true;
}

Ensuite, modifiez mouseReleaseEvent pour effacer l'indicateur et réinitialiser l'état interne :

void AxesInputHandler::mouseReleaseEvent(QMouseEvent *event, const QPoint &mousePos)
{
    Q3DInputHandler::mouseReleaseEvent(event, mousePos);
    m_mousePressed = false;
    m_state = StateNormal;
}

Ensuite, modifiez mouseMoveEvent. Vérifiez si l'indicateur m_mousePressed est true et si l'état interne est différent de StateNormal. Si c'est le cas, définissez les positions d'entrée pour le calcul de la distance de déplacement de la souris et appelez la fonction de déplacement de l'axe (voir Implémentation du déplacement de l'axe pour plus de détails) :

void AxesInputHandler::mouseMoveEvent(QMouseEvent *event, const QPoint &mousePos)
{
    // Check if we're trying to drag axis label
    if (m_mousePressed && m_state != StateNormal) {
        setPreviousInputPos(inputPosition());
        setInputPosition(mousePos);
        handleAxisDragging();
    } else {
        Q3DInputHandler::mouseMoveEvent(event, mousePos);
    }
}
Mise en œuvre du glissement d'axe

Tout d'abord, commencez à écouter le signal de sélection provenant du graphique. Faites-le dans le constructeur et connectez-le à la méthode handleElementSelected:

// Connect to the item selection signal from graph
connect(graph, &QAbstract3DGraph::selectedElementChanged, this,
        &AxesInputHandler::handleElementSelected);

Dans handleElementSelected, vérifiez le type de la sélection et définissez l'état interne en fonction de celui-ci :

switch (type) {
case QAbstract3DGraph::ElementAxisXLabel:
    m_state = StateDraggingX;
    break;
case QAbstract3DGraph::ElementAxisYLabel:
    m_state = StateDraggingY;
    break;
case QAbstract3DGraph::ElementAxisZLabel:
    m_state = StateDraggingZ;
    break;
default:
    m_state = StateNormal;
    break;
}

La logique de déplacement proprement dite est mise en œuvre dans la méthode handleAxisDragging, qui est appelée à partir de mouseMoveEvent, si les conditions requises sont remplies :

// Check if we're trying to drag axis label
if (m_mousePressed && m_state != StateNormal) {

Dans handleAxisDragging, il faut d'abord obtenir l'orientation de la scène à partir de la caméra active :

// Get scene orientation from active camera
float xRotation = scene()->activeCamera()->xRotation();
float yRotation = scene()->activeCamera()->yRotation();

Ensuite, il calcule les modificateurs de la direction du mouvement de la souris en fonction de l'orientation :

// Calculate directional drag multipliers based on rotation
float xMulX = qCos(qDegreesToRadians(xRotation));
float xMulY = qSin(qDegreesToRadians(xRotation));
float zMulX = qSin(qDegreesToRadians(xRotation));
float zMulY = qCos(qDegreesToRadians(xRotation));

Ensuite, calculez le mouvement de la souris et modifiez-le en fonction de la rotation y de la caméra :

// Get the drag amount
QPoint move = inputPosition() - previousInputPos();

// Flip the effect of y movement if we're viewing from below
float yMove = (yRotation < 0) ? -move.y() : move.y();

Ensuite, appliquez la distance déplacée à l'axe correct :

// Adjust axes
switch (m_state) {
case StateDraggingX:
    distance = (move.x() * xMulX - yMove * xMulY) / m_speedModifier;
    m_axisX->setRange(m_axisX->min() - distance, m_axisX->max() - distance);
    break;
case StateDraggingZ:
    distance = (move.x() * zMulX + yMove * zMulY) / m_speedModifier;
    m_axisZ->setRange(m_axisZ->min() + distance, m_axisZ->max() + distance);
    break;
case StateDraggingY:
    distance = move.y() / m_speedModifier; // No need to use adjusted y move here
    m_axisY->setRange(m_axisY->min() + distance, m_axisY->max() + distance);
    break;
default:
    break;
}

Enfin, ajoutez une fonction permettant de définir la vitesse de déplacement :

inline void setDragSpeedModifier(float modifier) { m_speedModifier = modifier; }

Cette fonction est nécessaire, car la distance de déplacement de la souris est absolue en coordonnées d'écran, et vous devez l'ajuster à la plage de l'axe. Plus la valeur est grande, plus le glissement sera lent. Notez que dans cet exemple, le niveau de zoom de la scène n'est pas pris en compte lors de la détermination de la vitesse de glissement, de sorte que vous remarquerez des changements dans l'ajustement de la portée lorsque vous modifiez le niveau de zoom.

Vous pouvez également ajuster le modificateur automatiquement en fonction de la portée de l'axe et du niveau de zoom de la caméra.

Graphique de surface

Dans l'onglet Surface Graph, créez un graphique de surface en 3D à l'aide de Q3DSurface. L'exemple montre comment :

  • Configurer un site QSurfaceDataProxy de base et définir les données correspondantes.
  • Utiliser QHeightMapSurfaceDataProxy pour afficher des cartes d'altitude en 3D.
  • Utiliser des données topographiques pour créer des cartes d'altitude en 3D.
  • Utiliser trois modes de sélection différents pour étudier le graphique.
  • Utilisez des plages d'axes pour afficher des parties sélectionnées du graphique.
  • Définir un gradient de surface personnalisé.
  • Ajouter des éléments et des étiquettes personnalisés avec QCustom3DItem et QCustom3DLabel.
  • Utiliser un gestionnaire d'entrée personnalisé pour activer le zoom et le panoramique.
  • Mettre en évidence une zone de la surface.

Pour la création d'une application de base, voir Bar Graph.

Surface simple avec données générées

Tout d'abord, instanciez un nouveau QSurfaceDataProxy et attachez-le à un nouveau QSurface3DSeries:

m_sqrtSinProxy = new QSurfaceDataProxy();
m_sqrtSinSeries = new QSurface3DSeries(m_sqrtSinProxy);

Ensuite, remplissez le proxy avec une simple racine carrée et des données sinusoïdales. Créez une nouvelle instance QSurfaceDataArray et ajoutez-y des éléments QSurfaceDataRow. Définissez l'instance QSurfaceDataArray créée comme tableau de données pour l'instance QSurfaceDataProxy en appelant resetArray().

auto *dataArray = new QSurfaceDataArray;
dataArray->reserve(sampleCountZ);
for (int i = 0 ; i < sampleCountZ ; ++i) {
    auto *newRow = new QSurfaceDataRow;
    newRow->reserve(sampleCountX);
    // Keep values within range bounds, since just adding step can cause minor drift due
    // to the rounding errors.
    float z = qMin(sampleMax, (i * stepZ + sampleMin));
    for (int j = 0; j < sampleCountX; ++j) {
        float x = qMin(sampleMax, (j * stepX + sampleMin));
        float R = qSqrt(z * z + x * x) + 0.01f;
        float y = (qSin(R) / R + 0.24f) * 1.61f;
        newRow->append(QSurfaceDataItem({x, y, z}));
    }
    dataArray->append(newRow);
}

m_sqrtSinProxy->resetArray(dataArray);
Données de la carte d'altitude multisérie

Créez la carte des hauteurs en instanciant un QHeightMapSurfaceDataProxy avec un QImage contenant les données sur les hauteurs. Utilisez QHeightMapSurfaceDataProxy::setValueRanges() pour définir la plage de valeurs de la carte. Dans l'exemple, la carte provient d'une position imaginaire de 34,0° N - 40,0° N et 18,0° E - 24,0° E. Ces valeurs sont utilisées pour positionner la carte sur les axes.

// Create the first surface layer
QImage heightMapImageOne(":/data/layer_1.png");
m_heightMapProxyOne = new QHeightMapSurfaceDataProxy(heightMapImageOne);
m_heightMapSeriesOne = new QSurface3DSeries(m_heightMapProxyOne);
m_heightMapSeriesOne->setItemLabelFormat(u"(@xLabel, @zLabel): @yLabel"_s);
m_heightMapProxyOne->setValueRanges(34.f, 40.f, 18.f, 24.f);

Ajoutez les autres couches de surface de la même manière, en créant un proxy et une série pour eux en utilisant des images de cartes altimétriques.

Données de la carte topographique

Les données topographiques proviennent du National Land Survey de Finlande. Il fournit un produit appelé Elevation Model 2 m, qui convient à cet exemple. Les données topographiques proviennent de Levi fell. La précision des données dépasse largement les besoins, c'est pourquoi elles sont compressées et encodées dans un fichier PNG. La valeur de hauteur des données ASCII d'origine est encodée au format RVB à l'aide d'un multiplicateur, que vous verrez plus loin dans un extrait de code. Le multiplicateur est calculé en divisant la plus grande valeur de 24 bits par le point le plus élevé en Finlande.

QHeightMapSurfaceDataProxy ne convertit que les valeurs d'un octet. Pour utiliser la plus grande précision des données du National Land Survey of Finland, lisez les données du fichier PNG et décodez-les à l'adresse QSurface3DSeries.

Commencez par définir le multiplicateur d'encodage :

// Value used to encode height data as RGB value on PNG file
const float packingFactor = 11983.f;

Ensuite, effectuez le décodage proprement dit :

QImage heightMapImage(file);
uchar *bits = heightMapImage.bits();
int imageHeight = heightMapImage.height();
int imageWidth = heightMapImage.width();
int widthBits = imageWidth * 4;
float stepX = width / float(imageWidth);
float stepZ = height / float(imageHeight);

auto *dataArray = new QSurfaceDataArray;
dataArray->reserve(imageHeight);
for (int i = 0; i < imageHeight; ++i) {
    int p = i * widthBits;
    float z = height - float(i) * stepZ;
    auto *newRow = new QSurfaceDataRow;
    newRow->reserve(imageWidth);
    for (int j = 0; j < imageWidth; ++j) {
        uchar aa = bits[p + 0];
        uchar rr = bits[p + 1];
        uchar gg = bits[p + 2];
        uint color = uint((gg << 16) + (rr << 8) + aa);
        float y = float(color) / packingFactor;
        newRow->append(QSurfaceDataItem({float(j) * stepX, y, z}));
        p += 4;
    }
    dataArray->append(newRow);
}

dataProxy()->resetArray(dataArray);

Les données sont maintenant utilisables par le proxy.

Sélection de la série de données

Afin de présenter différents proxy, Surface Graph dispose de trois boutons radio permettant de passer d'une série à l'autre.

Avec Sqrt & Sin, la série générée simple est activée. Commencez par définir les caractéristiques décoratives, telles que l'activation de la grille pour la surface et la sélection du mode d'ombrage plat. Définissez ensuite le format de l'étiquette de l'axe et les plages de valeurs. Paramétrez la rotation automatique des étiquettes pour améliorer leur lisibilité lorsque l'angle de la caméra est faible. Enfin, assurez-vous que la bonne série est ajoutée au graphique et que les autres ne le sont pas :

m_sqrtSinSeries->setDrawMode(QSurface3DSeries::DrawSurfaceAndWireframe);
m_sqrtSinSeries->setFlatShadingEnabled(true);

m_graph->axisX()->setLabelFormat("%.2f");
m_graph->axisZ()->setLabelFormat("%.2f");
m_graph->axisX()->setRange(sampleMin, sampleMax);
m_graph->axisY()->setRange(0.f, 2.f);
m_graph->axisZ()->setRange(sampleMin, sampleMax);
m_graph->axisX()->setLabelAutoRotation(30.f);
m_graph->axisY()->setLabelAutoRotation(90.f);
m_graph->axisZ()->setLabelAutoRotation(30.f);

m_graph->removeSeries(m_heightMapSeriesOne);
m_graph->removeSeries(m_heightMapSeriesTwo);
m_graph->removeSeries(m_heightMapSeriesThree);
m_graph->removeSeries(m_topography);
m_graph->removeSeries(m_highlight);

m_graph->addSeries(m_sqrtSinSeries);

Avec Multiseries Height Map, les séries de la carte d'altitude sont activées et les autres désactivées. L'ajustement automatique de la plage de l'axe Y fonctionne bien pour la surface de la carte d'altitude, assurez-vous donc qu'il est activé.

m_graph->axisY()->setAutoAdjustRange(true);

Avec Textured Topography, la série de cartes topographiques est activée et les autres sont désactivées. Activez un gestionnaire d'entrée personnalisé pour cette série, afin de pouvoir mettre des zones en surbrillance :

m_graph->setActiveInputHandler(m_customInputHandler);

Voir Utiliser un gestionnaire d'entrée personnalisé pour activer le zoom et le panoramique pour obtenir des informations sur le gestionnaire d'entrée personnalisé pour cette série de données.

Modes de sélection

Les trois modes de sélection pris en charge par Q3DSurface peuvent être utilisés avec des boutons radio. Pour activer le mode sélectionné ou l'effacer, ajoutez les méthodes en ligne suivantes :

void toggleModeNone() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionNone); }
void toggleModeItem() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItem); }
void toggleModeSliceRow() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndRow
                                                      | QAbstract3DGraph::SelectionSlice
                                                      | QAbstract3DGraph::SelectionMultiSeries); }
void toggleModeSliceColumn() { m_graph->setSelectionMode(QAbstract3DGraph::SelectionItemAndColumn
                                                         | QAbstract3DGraph::SelectionSlice
                                                         | QAbstract3DGraph::SelectionMultiSeries); }

Ajoutez les drapeaux QAbstract3DGraph::SelectionSlice et QAbstract3DGraph::SelectionMultiSeries pour les modes de sélection par ligne et par colonne afin de permettre une sélection par tranche de toutes les séries visibles du graphique simultanément.

Plages d'axes pour l'étude du graphique

L'exemple comporte quatre curseurs permettant d'ajuster les valeurs minimales et maximales des axes X et Z. Lors de la sélection du proxy, ces curseurs peuvent être réglés à l'aide de la souris. Lors de la sélection du proxy, ces curseurs sont ajustés pour correspondre aux plages d'axes de l'ensemble de données actuel :

// Reset range sliders for Sqrt & Sin
m_rangeMinX = sampleMin;
m_rangeMinZ = sampleMin;
m_stepX = (sampleMax - sampleMin) / float(sampleCountX - 1);
m_stepZ = (sampleMax - sampleMin) / float(sampleCountZ - 1);
m_axisMinSliderX->setMinimum(0);
m_axisMinSliderX->setMaximum(sampleCountX - 2);
m_axisMinSliderX->setValue(0);
m_axisMaxSliderX->setMinimum(1);
m_axisMaxSliderX->setMaximum(sampleCountX - 1);
m_axisMaxSliderX->setValue(sampleCountX - 1);
m_axisMinSliderZ->setMinimum(0);
m_axisMinSliderZ->setMaximum(sampleCountZ - 2);
m_axisMinSliderZ->setValue(0);
m_axisMaxSliderZ->setMinimum(1);
m_axisMaxSliderZ->setMaximum(sampleCountZ - 1);
m_axisMaxSliderZ->setValue(sampleCountZ - 1);

Ajouter au graphique la prise en charge de la définition de la plage X à partir des commandes du widget :

void SurfaceGraphModifier::setAxisXRange(float min, float max)
{
    m_graph->axisX()->setRange(min, max);
}

Ajouter la prise en charge de la plage Z de la même manière.

Gradients de surface personnalisés

Avec l'ensemble de données Sqrt & Sin, les gradients de surface personnalisés peuvent être utilisés à l'aide de deux boutons-poussoirs. Définissez le dégradé à l'aide de QLinearGradient, où les couleurs souhaitées sont définies. Changez également le style de couleur en Q3DTheme::ColorStyleRangeGradient pour utiliser le dégradé.

QLinearGradient gr;
gr.setColorAt(0.f, Qt::black);
gr.setColorAt(0.33f, Qt::blue);
gr.setColorAt(0.67f, Qt::red);
gr.setColorAt(1.f, Qt::yellow);

m_sqrtSinSeries->setBaseGradient(gr);
m_sqrtSinSeries->setColorStyle(Q3DTheme::ColorStyleRangeGradient);
Ajout de maillages personnalisés à l'application

Ajoutez les fichiers de maillage à CMakeLists.txt pour la construction de cmake :

set(graphgallery_resource_files
    ...
    "data/oilrig.obj"
    "data/pipe.obj"
    "data/refinery.obj"
    ...
)

qt6_add_resources(graphgallery "graphgallery"
    PREFIX
        "/"
    FILES
        ${graphgallery_resource_files}
)

Ajoutez-les également au fichier de ressources qrc pour une utilisation avec qmake :

<RCC>
    <qresource prefix="/">
        ...
        <file>data/refinery.obj</file>
        <file>data/oilrig.obj</file>
        <file>data/pipe.obj</file>
        ...
    </qresource>
</RCC>
Ajout d'éléments personnalisés à un graphique

Avec l'ensemble de données Multiseries Height Map, les éléments personnalisés sont insérés dans le graphique et peuvent être activés ou désactivés à l'aide de cases à cocher. D'autres qualités visuelles peuvent également être contrôlées à l'aide d'une autre série de cases à cocher, notamment la transparence pour les deux couches supérieures et la mise en évidence pour la couche inférieure.

Commencez par créer un petit QImage. Remplissez-le d'une seule couleur qui servira de couleur à l'objet personnalisé :

QImage color = QImage(2, 2, QImage::Format_RGB32);
color.fill(Qt::red);

Spécifiez ensuite la position de l'objet dans une variable. La position peut ensuite être utilisée pour retirer le bon élément du graphique :

QVector3D positionOne = QVector3D(39.f, 77.f, 19.2f);

Créez ensuite un nouveau site QCustom3DItem avec tous les paramètres :

auto *item = new QCustom3DItem(":/data/oilrig.obj", positionOne,
                               QVector3D(0.025f, 0.025f, 0.025f),
                               QQuaternion::fromAxisAndAngle(0.f, 1.f, 0.f, 45.f),
                               color);

Enfin, ajoutez l'élément au graphique :

m_graph->addCustomItem(item);
Ajout d'une étiquette personnalisée à un graphique

L'ajout d'une étiquette personnalisée est très similaire à l'ajout d'un élément personnalisé. Pour l'étiquette, il n'est pas nécessaire de créer un maillage personnalisé, mais simplement une instance QCustom3DLabel:

auto *label = new QCustom3DLabel();
label->setText("Oil Rig One");
label->setPosition(positionOneLabel);
label->setScaling(QVector3D(1.f, 1.f, 1.f));
m_graph->addCustomItem(label);
Suppression d'un élément personnalisé d'un graphique

Pour supprimer un élément spécifique du graphique, appelez removeCustomItemAt() avec la position de l'élément :

m_graph->removeCustomItemAt(positionOne);

Remarque : la suppression d'un élément personnalisé du graphique entraîne également la suppression de l'objet. Si vous souhaitez conserver l'élément, utilisez plutôt la méthode releaseCustomItem().

Texture d'une série de surface

Avec le jeu de données Textured Topography, créez une texture de carte à utiliser avec la carte d'altitude topographique.

Définissez une image à utiliser comme texture sur une surface avec QSurface3DSeries::setTextureFile(). Ajoutez une case à cocher pour contrôler si la texture est définie ou non, et un gestionnaire pour réagir à l'état de la case à cocher :

void SurfaceGraphModifier::toggleSurfaceTexture(bool enable)
{
    if (enable)
        m_topography->setTextureFile(":/data/maptexture.jpg");
    else
        m_topography->setTextureFile("");
}

Dans cet exemple, l'image est lue à partir d'un fichier JPG. La définition d'un fichier vide avec la méthode efface la texture, et la surface utilise les dégradés ou les couleurs du thème.

Utilisation d'un gestionnaire d'entrée personnalisé pour activer le zoom et le panoramique

Avec l'ensemble de données Textured Topography, créez un gestionnaire d'entrée personnalisé pour mettre en évidence la sélection sur le graphique et permettre un panoramique sur le graphique.

La mise en œuvre du panoramique est similaire à celle présentée dans la section Mise en œuvre du glissement d'axe. La différence est que, dans cet exemple, vous ne suivez que les axes X et Z et ne permettez pas de faire glisser la surface en dehors du graphique. Pour limiter le déplacement, suivez les limites des axes et ne faites rien si vous sortez du graphique :

case StateDraggingX:
    distance = (move.x() * xMulX - move.y() * xMulY) * m_speedModifier;
    m_axisXMinValue -= distance;
    m_axisXMaxValue -= distance;
    if (m_axisXMinValue < m_areaMinValue) {
        float dist = m_axisXMaxValue - m_axisXMinValue;
        m_axisXMinValue = m_areaMinValue;
        m_axisXMaxValue = m_axisXMinValue + dist;
    }
    if (m_axisXMaxValue > m_areaMaxValue) {
        float dist = m_axisXMaxValue - m_axisXMinValue;
        m_axisXMaxValue = m_areaMaxValue;
        m_axisXMinValue = m_axisXMaxValue - dist;
    }
    m_axisX->setRange(m_axisXMinValue, m_axisXMaxValue);
    break;

Pour le zoom, attrapez le site wheelEvent et ajustez les plages des axes X et Y en fonction de la valeur delta sur QWheelEvent. Ajustez l'axe Y de manière à ce que le rapport d'aspect entre l'axe Y et le plan XZ reste le même. Cela permet d'éviter d'obtenir un graphique dans lequel la hauteur est exagérée :

void CustomInputHandler::wheelEvent(QWheelEvent *event)
{
    float delta = float(event->angleDelta().y());

    m_axisXMinValue += delta;
    m_axisXMaxValue -= delta;
    m_axisZMinValue += delta;
    m_axisZMaxValue -= delta;
    checkConstraints();

    float y = (m_axisXMaxValue - m_axisXMinValue) * m_aspectRatio;

    m_axisX->setRange(m_axisXMinValue, m_axisXMaxValue);
    m_axisY->setRange(100.f, y);
    m_axisZ->setRange(m_axisZMinValue, m_axisZMaxValue);
}

Ensuite, ajoutez quelques limites au niveau de zoom, afin qu'il ne s'approche pas trop de la surface ou qu'il ne s'en éloigne pas trop. Par exemple, si la valeur de l'axe X est inférieure à la limite autorisée, c'est-à-dire si le zoom va trop loin, la valeur est fixée à la valeur minimale autorisée. Si la plage passe en dessous du minimum autorisé, les deux extrémités de l'axe sont ajustées de manière à ce que la plage reste à la limite :

if (m_axisXMinValue < m_areaMinValue)
    m_axisXMinValue = m_areaMinValue;
if (m_axisXMaxValue > m_areaMaxValue)
    m_axisXMaxValue = m_areaMaxValue;
// Don't allow too much zoom in
if ((m_axisXMaxValue - m_axisXMinValue) < m_axisXMinRange) {
    float adjust = (m_axisXMinRange - (m_axisXMaxValue - m_axisXMinValue)) / 2.f;
    m_axisXMinValue -= adjust;
    m_axisXMaxValue += adjust;
}
Mise en évidence d'une zone de la surface

Pour mettre en œuvre une mise en évidence à afficher sur la surface, créez une copie de la série et ajoutez un certain décalage à la valeur y. Dans cet exemple, la classe HighlightSeries implémente la création de la copie dans sa méthode handlePositionChange.

Tout d'abord, donnez à HighlightSeries le pointeur vers la série originale, puis commencez à écouter le signal QSurface3DSeries::selectedPointChanged:

void HighlightSeries::setTopographicSeries(TopographicSeries *series)
{
    m_topographicSeries = series;
    m_srcWidth = m_topographicSeries->dataProxy()->array()->at(0)->size();
    m_srcHeight = m_topographicSeries->dataProxy()->array()->size();

    QObject::connect(m_topographicSeries, &QSurface3DSeries::selectedPointChanged,
                     this, &HighlightSeries::handlePositionChange);
}

Lorsque le signal se déclenche, vérifiez que la position est valide. Ensuite, calculez les plages pour la zone copiée et vérifiez qu'elles restent dans les limites. Enfin, remplissez le tableau de données de la série "highlight" avec la plage du tableau de données de la série "topographie" :

void HighlightSeries::handlePositionChange(const QPoint &position)
{
    m_position = position;

    if (position == invalidSelectionPosition()) {
        setVisible(false);
        return;
    }

    int halfWidth = m_width / 2;
    int halfHeight = m_height / 2;

    int startX = position.y() - halfWidth;
    if (startX < 0 )
        startX = 0;
    int endX = position.y() + halfWidth;
    if (endX > (m_srcWidth - 1))
        endX = m_srcWidth - 1;
    int startZ = position.x() - halfHeight;
    if (startZ < 0 )
        startZ = 0;
    int endZ = position.x() + halfHeight;
    if (endZ > (m_srcHeight - 1))
        endZ = m_srcHeight - 1;

    QSurfaceDataProxy *srcProxy = m_topographicSeries->dataProxy();
    const QSurfaceDataArray &srcArray = *srcProxy->array();

    auto *dataArray = new QSurfaceDataArray;
    dataArray->reserve(endZ - startZ);
    for (int i = startZ; i < endZ; ++i) {
        auto *newRow = new QSurfaceDataRow;
        newRow->reserve(endX - startX);
        QSurfaceDataRow *srcRow = srcArray.at(i);
        for (int j = startX; j < endX; ++j) {
            QVector3D pos = srcRow->at(j).position();
            pos.setY(pos.y() + 0.1f);
            newRow->append(QSurfaceDataItem(pos));
        }
        dataArray->append(newRow);
    }

    dataProxy()->resetArray(dataArray);
    setVisible(true);
}
Un dégradé pour la série Highlight

Comme le site HighlightSeries est QSurface3DSeries, toutes les méthodes de décoration d'une série sont disponibles. Dans cet exemple, nous avons ajouté un dégradé pour mettre en évidence l'élévation. Étant donné que le style de dégradé approprié dépend de la plage de l'axe Y et que nous modifions la plage lorsque nous effectuons un zoom, les positions des couleurs de dégradé doivent être ajustées en fonction de la modification de la plage. Pour ce faire, définissez des valeurs proportionnelles pour les positions des couleurs du dégradé :

const float darkRedPos = 1.f;
const float redPos = 0.8f;
const float yellowPos = 0.6f;
const float greenPos = 0.4f;
const float darkGreenPos = 0.2f;

La modification du dégradé est effectuée dans la méthode handleGradientChange, il faut donc la connecter pour qu'elle réagisse aux changements de l'axe Y :

QObject::connect(m_graph->axisY(), &QValue3DAxis::maxChanged,
                 m_highlight, &HighlightSeries::handleGradientChange);

Lorsqu'une modification de la valeur maximale de l'axe Y se produit, calculez les nouvelles positions des couleurs du dégradé :

void HighlightSeries::handleGradientChange(float value)
{
    float ratio = m_minHeight / value;

    QLinearGradient gr;
    gr.setColorAt(0.f, Qt::black);
    gr.setColorAt(darkGreenPos * ratio, Qt::darkGreen);
    gr.setColorAt(greenPos * ratio, Qt::green);
    gr.setColorAt(yellowPos * ratio, Qt::yellow);
    gr.setColorAt(redPos * ratio, Qt::red);
    gr.setColorAt(darkRedPos * ratio, Qt::darkRed);

    setBaseGradient(gr);
    setColorStyle(Q3DTheme::ColorStyleRangeGradient);
}

Contenu de l'exemple

Exemple de projet @ 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.