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.