Visualisation de données avec QCustomPlot en QML et Qt

La bibliothèque QCustomPlot offre une solution native pour générer des tracés 2D de haute qualité dans une application Qt. Grâce à son intégration possible avec QML via un QQuickPaintedItem, on peut l’exploiter dans des interfaces modernes sans renoncer à la performance ni à la flexibilité du C++. Ce guide présente les étapes d’intégration et plusieurs exemples de visualisation : courbes, nuages de points, diagrammes à barres empilées, axes multiples et vues temps réel.

Mise en place du projet

Créez une application Qt Quick, puis ajoutez les fichiers qcustomplot.cpp et qcustomplot.h (téléchargeables sur le site officiel). Dans le fichier .pro, remplacez QGuiApplication par QApplication et ajoutez QT += printsupport.


// main.cpp
#include <QApplication>
#include <QQmlApplicationEngine>
#include "PlotItem.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    qmlRegisterType<PlotItem>("CustomPlot", 1, 0, "PlotItem");
    QQmlApplicationEngine engine;
    engine.load(QUrl("qrc:/main.qml"));
    return app.exec();
}

Item QML personnalisé

La classe PlotItem hérite de QQuickPaintedItem et embarque une instance de QCustomPlot. La méthode paint() redirige le rendu sur un QPixmap intermédiaire. Une fonction Q_INVOKABLE permet d’initailiser les graphiques depuis le QML.


// PlotItem.h (extrait)
class PlotItem : public QQuickPaintedItem {
    Q_OBJECT
public:
    explicit PlotItem(QQuickItem *parent = nullptr);
    void paint(QPainter *painter) override;
    Q_INVOKABLE void prepareChart();
private:
    QCustomPlot *m_plot = nullptr;
};


// PlotItem.cpp
PlotItem::PlotItem(QQuickItem *parent) : QQuickPaintedItem(parent) {
    setAcceptedMouseButtons(Qt::AllButtons);
    m_plot = new QCustomPlot;
}

void PlotItem::paint(QPainter *painter) {
    if (!m_plot) return;
    QPixmap buffer(size().toSize());
    QCPPainter qcp(&buffer);
    m_plot->toPainter(&qcp);
    painter->drawPixmap(0, 0, buffer);
}

Exemple 1 : fonction exponentielle amortie

Le code suivant génère deux séries : une onde cosinusoïdale décroissante et son enveloppe. On remplit la première courbe avec une couleur semi-transparente.


void PlotItem::prepareChart() {
    m_plot->addGraph();
    m_plot->graph(0)->setPen(QPen(QColor(0, 128, 255)));
    m_plot->graph(0)->setBrush(QBrush(QColor(0, 128, 255, 40)));
    m_plot->addGraph();
    m_plot->graph(1)->setPen(QPen(Qt::darkRed));

    QVector<double> xVals(300), signal(300), envelope(300);
    for (int i = 0; i < 300; ++i) {
        xVals[i] = i;
        signal[i] = std::exp(-i / 200.0) * std::cos(i / 12.0);
        envelope[i] = std::exp(-i / 200.0);
    }
    m_plot->graph(0)->setData(xVals, signal);
    m_plot->graph(1)->setData(xVals, envelope);
    m_plot->xAxis2->setVisible(true);
    m_plot->yAxis2->setVisible(true);
    m_plot->xAxis2->setTickLabels(false);
    m_plot->yAxis2->setTickLabels(false);
    m_plot->graph(0)->rescaleAxes();
    m_plot->graph(1)->rescaleAxes(true);
    m_plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
}

Exemple 2 : dispersion avec bande de confiance

On combine une courbe théorique (sinc) avec des points mesurés, une bande de confiance et des barres d’erreur.


void PlotItem::prepareChart() {
    m_plot->legend->setVisible(true);
    m_plot->legend->setFont(QFont("Verdana", 9));
    m_plot->setLocale(QLocale::English);

    m_plot->addGraph(); // bande supérieure
    QPen hatchPen(QColor(150,150,150), 1, Qt::DotLine);
    m_plot->graph(0)->setPen(hatchPen);
    m_plot->graph(0)->setBrush(QColor(255,100,30,30));
    m_plot->addGraph(); // bande inférieure
    m_plot->graph(1)->setPen(hatchPen);
    m_plot->graph(0)->setChannelFillGraph(m_plot->graph(1));

    m_plot->addGraph(); // courbe théorique
    m_plot->graph(2)->setPen(QPen(Qt::darkCyan, 2, Qt::DashLine));
    m_plot->graph(2)->setName("Modèle");

    m_plot->addGraph(); // points de mesure
    m_plot->graph(3)->setPen(QPen(Qt::blue));
    m_plot->graph(3)->setLineStyle(QCPGraph::lsNone);
    m_plot->graph(3)->setScatterStyle(QCPScatterStyle::ssCircle, 5);
    m_plot->graph(3)->setName("Mesures");

    QCPErrorBars *err = new QCPErrorBars(m_plot->xAxis, m_plot->yAxis);
    err->setDataPlottable(m_plot->graph(3));
    err->setPen(QPen(QColor(180,180,180)));

    QVector<double> x0(300), y0(300), upper(300), lower(300);
    for (int i=0; i<300; ++i) {
        x0[i] = (i/299.0 - 0.5) * 25.0 + 0.01;
        y0[i] = std::sin(x0[i]) / x0[i];
        upper[i] = y0[i] + 0.12;
        lower[i] = y0[i] - 0.12;
        x0[i] *= 1000;
    }
    QVector<double> x1(80), y1(80), errVals(80);
    for (int i=0; i<80; ++i) {
        double tmp = (double)rand() / RAND_MAX * 2 * M_PI;
        double bruit = std::sqrt(-2*std::log((double)rand()/RAND_MAX)) * std::cos(tmp);
        x1[i] = (i/79.0 - 0.5) * 25.0 + 0.3;
        y1[i] = std::sin(x1[i])/x1[i] + bruit * 0.12;
        x1[i] *= 1000;
        errVals[i] = 0.12;
    }
    m_plot->graph(0)->setData(x0, upper);
    m_plot->graph(1)->setData(x0, lower);
    m_plot->graph(2)->setData(x0, y0);
    m_plot->graph(3)->setData(x1, y1);
    err->setData(errVals);
    m_plot->graph(2)->rescaleAxes();
    m_plot->graph(3)->rescaleAxes(true);
    m_plot->axisRect()->setupFullAxesBox();
}

Exemple 3 : styles de dispersion

Ce fragment illustre plusieurs formes de marqueurs (QCPScatterStyle) avec des couleurs générées dynamiquement.


void PlotItem::prepareChart() {
    QVector<QCPScatterStyle::ScatterShape> formes;
    formes << QCPScatterStyle::ssCircle << QCPScatterStyle::ssSquare
           << QCPScatterStyle::ssDiamond << QCPScatterStyle::ssStar
           << QCPScatterStyle::ssTriangle << QCPScatterStyle::ssCross
           << QCPScatterStyle::ssPlus << QCPScatterStyle::ssDisc;

    for (int i = 0; i < formes.size(); ++i) {
        m_plot->addGraph();
        QColor col = QColor::fromHsv(i * 40, 200, 200);
        m_plot->graph()->setPen(QPen(col));
        QVector<double> xs(12), ys(12);
        for (int k = 0; k < 12; ++k) {
            xs[k] = k * 0.5;
            ys[k] = 5 * std::sin(xs[k]) + (formes.size() - i) * 2.5;
        }
        m_plot->graph()->setData(xs, ys);
        m_plot->graph()->setLineStyle(QCPGraph::lsLine);
        m_plot->graph()->setScatterStyle(QCPScatterStyle(formes[i], 8));
    }
    m_plot->xAxis->setTicks(false);
    m_plot->yAxis->setTicks(false);
    m_plot->axisRect()->setupFullAxesBox();
}

Exemple 4 : diagramme à barres empilé

On cnostruit trois séries empilées représentant différents types d’énergie par pays, avec un fond sombre et une légende personnalisée.


void PlotItem::prepareChart() {
    QLinearGradient grad(0, 0, 0, 400);
    grad.setColorAt(0, QColor(60,60,60));
    grad.setColorAt(0.5, QColor(80,80,80));
    grad.setColorAt(1, QColor(45,45,45));
    m_plot->setBackground(grad);

    QCPBars *solaire = new QCPBars(m_plot->xAxis, m_plot->yAxis);
    QCPBars *eolien  = new QCPBars(m_plot->xAxis, m_plot->yAxis);
    QCPBars *hydro   = new QCPBars(m_plot->xAxis, m_plot->yAxis);
    hydro->setName("Hydraulique");
    hydro->setBrush(QColor(20, 100, 200));
    eolien->setName("Éolien");
    eolien->setBrush(QColor(200, 150, 20));
    solaire->setName("Solaire");
    solaire->setBrush(QColor(200, 80, 20));
    eolien->moveAbove(hydro);
    solaire->moveAbove(eolien);

    QVector<double> positions{1, 2, 3, 4, 5};
    QVector<QString> labels{"FR", "DE", "ES", "IT", "UK"};
    QSharedPointer<QCPAxisTickerText> ticker(new QCPAxisTickerText);
    ticker->addTicks(positions, labels);
    m_plot->xAxis->setTicker(ticker);
    m_plot->xAxis->setRange(0, 6);

    QVector<double> dataSolaire{2.3, 3.1, 4.0, 2.8, 1.5};
    QVector<double> dataEolien{1.2, 2.4, 1.1, 0.9, 2.2};
    QVector<double> dataHydro{0.8, 0.6, 0.9, 0.3, 0.5};
    solaire->setData(positions, dataSolaire);
    eolien->setData(positions, dataEolien);
    hydro->setData(positions, dataHydro);

    m_plot->yAxis->setRange(0, 8);
    m_plot->legend->setVisible(true);
    m_plot->legend->setBrush(QColor(255,255,255,120));
    m_plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);
}

Exemple 5 : axes multiples avec sous-graphiques

On réorganise le layout pour placer un grand axe rectangulaire en haut et deux petits en bas, partageant les mêmes marges.


void PlotItem::prepareChart() {
    m_plot->plotLayout()->clear();
    QCPAxisRect *wideRect = new QCPAxisRect(m_plot);
    wideRect->setupFullAxesBox(true);
    QCPLayoutGrid *subLayout = new QCPLayoutGrid;
    m_plot->plotLayout()->addElement(0, 0, wideRect);
    m_plot->plotLayout()->addElement(1, 0, subLayout);

    QCPAxisRect *leftRect = new QCPAxisRect(m_plot, false);
    QCPAxisRect *rightRect = new QCPAxisRect(m_plot, false);
    subLayout->addElement(0, 0, leftRect);
    subLayout->addElement(0, 1, rightRect);
    rightRect->setMaximumSize(120,120);
    rightRect->setMinimumSize(120,120);
    leftRect->addAxes(QCPAxis::atBottom | QCPAxis::atLeft);
    rightRect->addAxes(QCPAxis::atBottom | QCPAxis::atRight);

    QCPMarginGroup *mg = new QCPMarginGroup(m_plot);
    leftRect->setMarginGroup(QCP::msLeft, mg);
    rightRect->setMarginGroup(QCP::msRight, mg);
    wideRect->setMarginGroup(QCP::msLeft | QCP::msRight, mg);

    // Données pour le graphique principal
    QVector<double> keys(60), values(60);
    for (int i=0; i<60; ++i) {
        keys[i] = i * 0.2;
        values[i] = 5 * std::exp(-keys[i]) * std::cos(keys[i] * 3);
    }
    auto mainGraph = m_plot->addGraph(wideRect->axis(QCPAxis::atBottom),
                                      wideRect->axis(QCPAxis::atLeft));
    mainGraph->setData(keys, values);
    mainGraph->rescaleAxes();

    // Petit graphique de gauche (impulsions)
    QVector<double> x2(30), y2(30);
    for (int i=0; i<30; ++i) {
        x2[i] = i;
        y2[i] = (rand() % 100) / 100.0;
    }
    auto subGraph = m_plot->addGraph(leftRect->axis(QCPAxis::atBottom),
                                     leftRect->axis(QCPAxis::atLeft));
    subGraph->setData(x2, y2);
    subGraph->rescaleAxes();

    // Petit graphique de droite (barres)
    QCPBars *bars = new QCPBars(rightRect->axis(QCPAxis::atBottom),
                               rightRect->axis(QCPAxis::atRight));
    QVector<double> cat{1,2,3,4}, vals{3.2, 5.6, 2.1, 4.8};
    bars->setData(cat, vals);
    bars->rescaleAxes();

    foreach (QCPAxisRect *r, m_plot->axisRects())
        foreach (QCPAxis *ax, r->axes()) {
            ax->setLayer("axes");
            ax->grid()->setLayer("grid");
        }
}

Utilisation autonome avec QWidget

On peut aussi afficher QCustomPlot dans une fenêtre traditionnelle, par exemple pour un tracé temps réel avec pause au clic.


// main.cpp – oscillateur dynamique
#include <QApplication>
#include <QTimer>
#include "qcustomplot.h"

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    QCustomPlot w;
    w.addGraph();
    w.graph(0)->setPen(QPen(Qt::green, 2));
    w.xAxis->setLabel("ms");
    w.yAxis->setRange(-1.5, 1.5);
    w.xAxis->setRange(0, 800);

    QTimer timer;
    QVector<double> t, s;
    int tick = 0;
    bool running = true;
    QObject::connect(&timer, &QTimer::timeout, [&](){
        double y = std::sin(tick * 0.02) * std::cos(tick * 0.008) + (rand()%40-20)/200.0;
        t.append(tick);
        s.append(y);
        while (!t.empty() && t.front() < tick - 800) {
            t.pop_front(); s.pop_front();
        }
        w.graph(0)->setData(t, s);
        w.xAxis->setRange(tick-800, tick);
        w.replot();
        tick += 5;
    });

    QObject::connect(&w, &QCustomPlot::mousePress, [&](QMouseEvent *ev){
        if (ev->button() == Qt::LeftButton) {
            running ? timer.stop() : timer.start(30);
            running = !running;
        }
    });
    timer.start(30);
    w.resize(800, 400);
    w.show();
    return app.exec();
}

Carte de chaleur

Un QCPColorMap permet de représenter une fonction de deux variables avec une échelle de couleurs continue.


// dans une fonction setupChart() pour un QWidget
QCPColorMap *heatmap = new QCPColorMap(customPlot->xAxis, customPlot->yAxis);
int nx = 120, ny = 120;
heatmap->data()->setSize(nx, ny);
heatmap->data()->setRange(QCPRange(-3, 3), QCPRange(-3, 3));
for (int i = 0; i < nx; ++i) {
    for (int j = 0; j < ny; ++j) {
        double x, y;
        heatmap->data()->cellToCoord(i, j, &x, &y);
        double z = std::cos(x) * std::exp(- (x*x + y*y) * 0.5);
        heatmap->data()->setCell(i, j, z);
    }
}
QCPColorGradient grad;
grad.setColorStopAt(0, QColor(0,0,180));
grad.setColorStopAt(1, QColor(255,30,30));
heatmap->setGradient(grad);

QCPColorScale *scale = new QCPColorScale(customPlot);
customPlot->plotLayout()->addElement(0, 1, scale);
heatmap->setColorScale(scale);
scale->setGradient(grad);
scale->setDataRange(QCPRange(-1, 1));
customPlot->rescaleAxes();

Le dépôt d’exemples officiel de QCustomPlot fournit de nombreuses autres démonstrations (graphiques financiers, polairse, etc.). En les adaptant au modèle QQuickPaintedItem, on peut construire des tableaux de bord interactifs, comme un système de supervision de drone avec plusieurs graphiques synchronisés et une vue 3D OpenGL.

Étiquettes: QCustomPlot QML Qt visualisation 2D QQuickPaintedItem

Publié le 6 octobre à 07h58