Création de Graphiques en Lignes Dynamiques avec Qwt et Qt C++

Fondamentaux de l'architecture Qwt

La bibliothèque Qwt est une extension incontournable pour le développement d'interfaces graphiques scientifiques et techniques sous Qt. Elle fournit un ensemble complet de widgets pour la visualisation de données 2D. Pour concevoir un graphique en lignes fonctionnel et performant, il est essentiel de comprendre l'interaction entre ses composants de base.

Composants principaux

  • QwtPlot : Le widget conteneur principal. Il gère le canevas de dessin, les axes (jusqu'à quatre) et les échelles. Il orchestre le rendu de tous les éléments graphiques qui y sont attachés.
  • QwtPlotCurve : Représente une série de points de données (coordonnées X et Y). Il gère l'interpolation, le style de trait et l'affichage des symboles sur les points de données.
  • QwtPlotGrid : Permet de dessiner une grille de fond composée de lignes majeures et mineures, alignées sur les divisions des axes.
  • QwtLegend : Le widget de légende qui liste les éléments du graphique. Il peut être configuré pour être en lecture seule ou interactif (cliquable pour masquer/afficher des courbes).
  • QwtSymbol : Définit la forme géométrique (cercle, carré, croix, etc.) dessinée sur chaque point de données d'une courbe.

Implémentation d'un graphique en temps réel

Pour illustrer le flux de travail, nous allons créer un widget personnalisé affichant deux courbes en temps réel. La première courbe est liée à l'axe Y gauche et affiche des valeurs oscillantes, tandis que la seconde est liée à l'axe Y droit pour des valeurs positives. L'axe X défile automatiquement pour maintenir une fenêtre temporelle visible constante.

En-tête du composant (DynamicLineChart.h)


#ifndef DYNAMICLINECHART_H
#define DYNAMICLINECHART_H

#include <QWidget>
#include <QElapsedTimer>
#include <QVector>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_legend.h>
#include <qwt_symbol.h>

class DynamicLineChart : public QWidget {
    Q_OBJECT

public:
    explicit DynamicLineChart(QWidget *parent = nullptr);
    ~DynamicLineChart() override = default;

protected:
    void timerEvent(QTimerEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;

private:
    void setupUi();
    void initializeCurves();
    void updateChartData();
    void adjustAxisScale();

    QwtPlot *m_plotWidget;
    QwtPlotCurve *m_curveAlpha;
    QwtPlotCurve *m_curveBeta;
    
    QVector<double> m_xValuesAlpha;
    QVector<double> m_yValuesAlpha;
    QVector<double> m_xValuesBeta;
    QVector<double> m_yValuesBeta;

    QElapsedTimer m_elapsedTimer;
    int m_dataUpdateTimerId;
    int m_scaleUpdateTimerId;
    
    static constexpr double VISIBLE_TIME_RANGE = 15.0;
};

#endif // DYNAMICLINECHART_H

Implémentation (DynamicLineChart.cpp)


#include "DynamicLineChart.h"
#include <QResizeEvent>
#include <QPen>
#include <cstdlib>

DynamicLineChart::DynamicLineChart(QWidget *parent) 
    : QWidget(parent), 
      m_plotWidget(nullptr), 
      m_curveAlpha(nullptr), 
      m_curveBeta(nullptr),
      m_dataUpdateTimerId(0),
      m_scaleUpdateTimerId(0) {
    
    setupUi();
    initializeCurves();
    
    m_elapsedTimer.start();
    
    // Timer pour l'ajout de points de données (toutes les 500ms)
    m_dataUpdateTimerId = startTimer(500);
    // Timer pour l'ajustement de l'échelle et le rafraîchissement (toutes les 50ms)
    m_scaleUpdateTimerId = startTimer(50);
}

void DynamicLineChart::setupUi() {
    m_plotWidget = new QwtPlot(this);
    m_plotWidget->setCanvasBackground(QBrush(QColor(250, 250, 250)));

    // Configuration des axes
    m_plotWidget->setAxisTitle(QwtPlot::xBottom, "Temps (s)");
    m_plotWidget->setAxisTitle(QwtPlot::yLeft, "Amplitude Alpha");
    m_plotWidget->setAxisTitle(QwtPlot::yRight, "Amplitude Beta");
    
    m_plotWidget->setAxisAutoScale(QwtPlot::yLeft);
    m_plotWidget->setAxisAutoScale(QwtPlot::yRight);
    m_plotWidget->enableAxis(QwtPlot::yRight);

    // Grille de fond
    QwtPlotGrid *grid = new QwtPlotGrid();
    grid->setMajorPen(QPen(Qt::lightGray, 0, Qt::DotLine));
    grid->attach(m_plotWidget);

    // Légende
    QwtLegend *legend = new QwtLegend();
    legend->setDefaultItemMode(QwtLegendData::Clickable);
    m_plotWidget->insertLegend(legend, QwtPlot::BottomLegend);
}

void DynamicLineChart::initializeCurves() {
    // Courbe Alpha
    m_curveAlpha = new QwtPlotCurve("Signal Alpha");
    m_curveAlpha->setPen(QPen(QColor(30, 144, 255), 2));
    m_curveAlpha->setRenderHint(QwtPlotItem::RenderAntialiased, true);
    m_curveAlpha->setXAxis(QwtPlot::xBottom);
    m_curveAlpha->setYAxis(QwtPlot::yLeft);
    
    QwtSymbol *symbol = new QwtSymbol(QwtSymbol::Ellipse, QBrush(Qt::white), QPen(Qt::blue, 1), QSize(6, 6));
    m_curveAlpha->setSymbol(symbol);
    m_curveAlpha->attach(m_plotWidget);

    // Courbe Beta
    m_curveBeta = new QwtPlotCurve("Signal Beta");
    m_curveBeta->setPen(QPen(QColor(220, 20, 60), 2));
    m_curveBeta->setRenderHint(QwtPlotItem::RenderAntialiased, true);
    m_curveAlpha->setXAxis(QwtPlot::xBottom);
    m_curveBeta->setYAxis(QwtPlot::yRight);
    m_curveBeta->attach(m_plotWidget);
}

void DynamicLineChart::updateChartData() {
    double currentTime = m_elapsedTimer.elapsed() / 1000.0;
    
    // Génération de données pseudo-aléatoires
    double valAlpha = (std::rand() % 200) / 10.0 - 10.0;
    double valBeta = std::rand() % 100;

    m_xValuesAlpha.append(currentTime);
    m_yValuesAlpha.append(valAlpha);
    
    m_xValuesBeta.append(currentTime);
    m_yValuesBeta.append(valBeta);

    // Réassignation des données pour déclencher la mise à jour interne
    m_curveAlpha->setSamples(m_xValuesAlpha, m_yValuesAlpha);
    m_curveBeta->setSamples(m_xValuesBeta, m_yValuesBeta);
}

void DynamicLineChart::adjustAxisScale() {
    double currentTime = m_elapsedTimer.elapsed() / 1000.0;
    
    if (currentTime < VISIBLE_TIME_RANGE) {
        m_plotWidget->setAxisScale(QwtPlot::xBottom, 0, VISIBLE_TIME_RANGE);
    } else {
        m_plotWidget->setAxisScale(QwtPlot::xBottom, currentTime - VISIBLE_TIME_RANGE, currentTime);
    }
    
    m_plotWidget->replot();
}

void DynamicLineChart::timerEvent(QTimerEvent *event) {
    if (event->timerId() == m_dataUpdateTimerId) {
        updateChartData();
    } else if (event->timerId() == m_scaleUpdateTimerId) {
        adjustAxisScale();
    }
    QWidget::timerEvent(event);
}

void DynamicLineChart::resizeEvent(QResizeEvent *event) {
    if (m_plotWidget) {
        m_plotWidget->setGeometry(rect());
    }
    QWidget::resizeEvent(event);
}

Piège courant : Mise à jour des données et partage implicite

Lors de l'implémentation de graphqiues en temps réel, un problème fréquent survient concernant la mise à jour des données via QVector. Les développeurs s'attendent souvent à ce que la modification du conteneur QVector passé à la courbe se répercute automatiquement sur le dessin, en raison du mécanisme de partage implicite (Implicit Sharing) de Qt.

Le problème : Si vous ajoutez des éléments à votre QVector sans réappeler setSamples(), le graphique ne se met pas à jour. L'objet QwtPlotCurve conserve une référence interne aux données et ne détecte pas les modifications structurelles (comme l'ajout de nouvelles valeurs) effectuées en dehors de son API.

La solution : Il est impératif de réaffecter les données à la courbe à chaque cycle de mise à jour. L'appel explicite à setSamples(m_xValues, m_yValues) après avoir modifié les vecteurs force Qwt à réévaluer les bornes et à invalider le cache de rendu, garantissant ainsi un affichage correct et fluide des nouveaux points de données.

Étiquettes: Qwt Qt C++ DataVisualization QwtPlot

Publié le 19 juillet à 01h46