Qt pour Android : Des graphiques avec Qt Charts


Version PDF de ce document : qt-android-charts.pdf


Qt fournit le module Qt Charts pour dessiner des graphiques.

On va l’utiliser dans l’exemple Qt pour Android : base de données SQLite pour obtenir ceci :

Remarque : Cet exemple existe aussi pour une base de données MySQL.

API Qt Charts

Pour utiliser le module Qt Charts, il faut l’activer, ainsi que le module widgets, dans le fichier de projet .pro :

...
QT += widgets charts
...

Il faut aussi modifier le fichier main.cpp pour utiliser QApplication à la place de QGuiApplication :

#include <QApplication>
#include <QIcon>
#include <QQmlApplicationEngine>
#include <QQmlContext>

#include "releve.h"

int main(int argc, char *argv[])
{
    QApplication::setApplicationName("MonApplicationBDCharts");
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QApplication app(argc, argv);

    QIcon::setThemeName("MonApplicationBDCharts");

    QQmlApplicationEngine engine;
    engine.rootContext()->setContextProperty("Releve", new Releve());
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;

    return app.exec();
}

Exemple

Dans l’exemple vu précédemment, on a créé une base de données mabase.sqlite comprenant 2 tables :

  • la table releves qui contiendra la description des relevés
  • la table mesures qui contiendra les mesures horodatées de températures pour chaque relevé
pragma foreign_keys = on;

CREATE TABLE releves ( 
        idReleve INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , 
        description VARCHAR(255) NULL
); 

CREATE TABLE mesures ( 
        idMesure INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL , 
        idReleve INTEGER NOT NULL , 
        horodatage DATETIME,
        temperature DOUBLE NULL , 
        CONSTRAINT fk_mesures_1 FOREIGN KEY (idReleve) REFERENCES releves (idReleve) ON DELETE CASCADE
);

Pour les tests, on a inséré quelques mesures :

INSERT INTO releves(idReleve,description) VALUES(1,'Relevé de la serre 1');
INSERT INTO mesures(idReleve,horodatage,temperature) VALUES(1,'2017-04-01 08:00:00',35.23);
...

INSERT INTO releves(idReleve,description) VALUES(2,'Relevé de la serre 2');
INSERT INTO mesures(idReleve,horodatage,temperature) VALUES(2,'2017-04-01 08:00:00',35.1);
...

Et quelques requêtes SQL que l’on utilise ensuite dans le code :

// la liste des relevés
SELECT description FROM releves ORDER BY releves.description ASC
// Les 5 dernières mesures du relevé 1
SELECT * FROM (SELECT mesures.horodatage, mesures.temperature FROM mesures INNER JOIN releves ON releves.idReleve = mesures.idReleve WHERE releves.description = 'Relevé de la serre 1' ORDER BY mesures.horodatage DESC LIMIT 5) tmp ORDER BY horodatage ASC LIMIT 5
// La moyenne des 5 dernières mesures du relevé 1
SELECT AVG(temperature) FROM (SELECT mesures.horodatage, mesures.temperature FROM mesures INNER JOIN releves ON releves.idReleve = mesures.idReleve WHERE releves.description = 'Relevé de la serre 1' ORDER BY mesures.horodatage DESC LIMIT 5) tmp ORDER BY horodatage ASC LIMIT 5

Projet Qt

On a créé un projet Qt QML Quick. Au final, le fichier .pro sera le suivant :

QT += widgets quick quickcontrols2 sql charts
CONFIG += c++11

HEADERS += \
    releve.h \
    mesure.h

SOURCES += main.cpp \
    releve.cpp \
    mesure.cpp

RESOURCES += qml.qrc \
             icons/MonApplicationBDCharts/index.theme \
             $$files(icons/*.png, true)

unix:!macx:
{
    android:
    {
        DISTFILES += \
            android-sources/AndroidManifest.xml
        ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android-sources
        # déploie la base de données avec l'apk
        deployment.files += mabase.sqlite
        deployment.path = /assets/db
        INSTALLS += deployment
    }
    !android:
    {
        # copie la base de données dans le dossier build
        CONFIG += file_copies
        COPIES += bd
        bd.files = mabase.sqlite
        bd.path = $$OUT_PWD/
        bd.base = $$PWD/
    }
}

Interactions C++/QML

On a ajouté une classe Releve qui a la charge d’ouvir la base de données mabase.sqlite, d’effectuer les 3 requêtes SQL et de fournir les résultats à l’IHM.

La classe Releve hérite de QObject afin de bénificier des mécanismes Qt. Les explications sur cette classe sont fournies dans l’exemple Qt pour Android : base de données SQLite.

#ifndef RELEVE_H
#define RELEVE_H

#include <QObject>
#include <QString>
#include <QtSql/QtSql>
#include <QSqlDatabase>

#define NOM_BD QString("mabase.sqlite")

class Releve : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool erreurConnexion MEMBER erreurConnexion NOTIFY erreurChanged)
    Q_PROPERTY(QStringList listeReleves READ getReleves NOTIFY listeRelevesChanged)
    Q_PROPERTY(QVariant mesures READ getMesures NOTIFY mesuresUpdated)
    Q_PROPERTY(QString moyenne READ getMoyenne NOTIFY moyenneUpdated)

public:
    explicit Releve(QObject *parent = nullptr);
    virtual ~Releve();

    Q_INVOKABLE bool lireReleve(QString releve, int nb=0);
    Q_INVOKABLE void lireMoyenneReleve(QString releve, int nb=0);
    Q_INVOKABLE bool executerRequete(QString requete);
    QStringList getReleves();
    QVariant getMesures();
    QString getMoyenne();

private:
    QSqlDatabase db;
    bool erreurConnexion;
    QStringList releves;
    QList<QObject*> mesures;
    QString moyenne;

    bool recuperer(QString requete, QStringList &donnees);
    bool recuperer(QString requete, QVector<QStringList> &donnees);
    bool recuperer(QString requete, QString &donnee);
    bool copier(QFile &sfile, QFile &dfile);
    bool remplacer(QFile &sfile, QFile &dfile);
    bool estBDPresente(QString BD);

signals:
    void erreurChanged();
    void listeRelevesChanged();
    void mesuresUpdated();
    void mesuresErreur();
    void moyenneUpdated();

public slots:
};

#endif // RELEVE_H

On a aussi créé une classe Mesure pour stocker une mesure qui est caractérisée par :

  • une température (un double)
  • et son horodatage (un QDateTime que l’on peut aussi retourner sous la forme d’un QString)

Par rapport à l’exemple de base, on a ajouté une propriété datetime de type QDateTime car on en aura besoin pour afficher l’horodatage sur l’axe X du graphique.

#ifndef MESURE_H
#define MESURE_H

#include <QObject>
#include <QDateTime>

class Mesure : public QObject
{
    Q_OBJECT
    Q_PROPERTY(double temperature READ getTemperature NOTIFY mesureChanged)
    Q_PROPERTY(QString horodatage READ getHorodatage NOTIFY mesureChanged)
    Q_PROPERTY(QDateTime datetime READ getDateTime NOTIFY mesureChanged)

public:
    explicit Mesure(QDateTime horodatage, double temperature, QObject *parent = nullptr);

    double getTemperature() const;
    QString getHorodatage() const;
    QDateTime getDateTime() const;

private:
    QDateTime horodatage;
    double temperature;

signals:
    void mesureChanged();

public slots:
};

#endif // MESURE_H

L’IHM

L’interface utilisateur est décrite en QML avec Qt Quick Controls 2.

La structure de base de la GUI est expliquée dans l’exemple Qt pour Android : Base de données SQLite.

L’affichage du relevé se fait dans Releve.qml. On crée un nouveau fichier QML Graphique.qml pour l’affichage du graphique.

Par rapport à la première version, on ajoute un bouton radio pour choisir le type d’affichage : sous forme de liste ou sous forme de graphique. Dans la version 2, on utilisera les possibilités de balayage (Flickable) de QML pour passer d’une vue à l’autre.

Dans Releve.qml, on ajoute tout d’abord le choix de la vue :

...
property bool liste: true
...
Row {
    spacing: 20
    ButtonGroup {
        id: choixAffichage
        onClicked: {
            if(button.text == "Liste")
                liste = true
            else
                liste = false
        }
    }
    RadioButton {
        text: "Liste"
        ButtonGroup.group: choixAffichage
        checked: liste
    }
    RadioButton {
        text: "Graphique"
        checked: !liste
        ButtonGroup.group: choixAffichage
    }
}
...

L’affichage des mesures sous fome de ListView se fera dans Mesures.qml.

Liens pour Qt Charts :

Pour le graphique, on crée un fichier Graphique.qml. L’affichage est basé sur un ChartView. On ajoute un LineSeries pour l’affichage de la courbe ainsi que 2 axes basés sur ValueAxis. Si l’on souhaite afficher l’horodatage, on utilisera un DateTimeAxis pour l’axe X.

La courbe LineSeries peut se personnaliser pour afficher les points de mesure (pointsVisible) et leurs valeurs (pointLabelsVisible et pointLabelsFormat). On lui donnera aussi un nom ( name) pour qu’il s’affiche dans la légende du graphique. Avec la propriété style, on peut choisir le type de ligne, par exemple Qt.DashLine pour l’affichage de la moyenne en pointillés).

import QtQuick 2.9
import QtCharts 2.0

Item {
    ...

    ChartView {
        id: graphique
        width: parent.width
        height: parent.height
        anchors.fill: parent
        theme: ChartView.ChartThemeBlueNcs
        animationOptions: ChartView.NoAnimation
        antialiasing: true

        ValueAxis { // températures
            id: axeY
            gridVisible: true
            labelFormat: "%.1f"
            tickCount: 6
        }

        /*
        // Numérotation de chaque valeur 1 .. n
        ValueAxis {
            id: axeX
            gridVisible: true
            labelFormat: "%d"
        }*/
        
        // Ou affichage de l'horodatage
        DateTimeAxis {
            id: axeX
            gridVisible: true
            format: "<center>dd/MM/yyyy<br> à hh:mm</center>"
            tickCount: 5
        }

        LineSeries {
            id: courbe
            name: "Température"
            axisX: axeX
            axisY: axeY
            pointsVisible: true
            pointLabelsVisible: true
            pointLabelsFormat: "@yPoint °C"
        }

        LineSeries {
            id: courbeMoyenne
            name: "Moyenne"
            color: "#0000FF"
            axisX: axeX
            axisY: axeY
            style: Qt.DashLine
            width: 2.5
        }
    }
}

Le plus important reste l’insertion des données (ici en provenance de la base de données) dans le graphique. Il faut mettre en oeuvre une interaction entre le code C++ et QML.

Dans cet exemple, on va utiliser une fonction JavaScript (un slot connecté au signal onMesuresUpdated). On récupère les données dans la propriété mesure de l’objet Releve que l’on ajoutera à la courbe avec la fonction append(). Côté JavaScript, on manipulera un tableau contenant des Mesure. Le nombre d’éléments du tableau s’obtient en utilisant la propriété length. On en profitera aussi pour déterminer la plage min et max de chaque axe.

Si on choisit l’affichage de l’horodatage, il faut passer par un objet Date côté JS afin d’y stocker le type QDateTime. Ensuite, on récupère le timestamp avec getTime(). Pour assurer un affichage correct de cet horodatage, il faudra bien veiller à fixer convenablement les valeur min et max de l’axe X.

Item {
    Connections {
        target: Releve
        onMesuresUpdated: {
            // on commence par effacer la courbe précédente
            courbe.clear()
            var horodatages = [] // pour min/max
            var temperatures = [] // pour min/max
            var horodatage = new Date() // pour le type QDateTime
            for(var i=0; i < Releve.mesures.length; i++)
            {
                //courbe.append(i+1, Releve.mesures[i].temperature)
                // ou :
                horodatage = Releve.mesures[i].datetime
                courbe.append(horodatage.getTime(), Releve.mesures[i].temperature)
                horodatages.push(horodatage.getTime())
                temperatures.push(Releve.mesures[i].temperature)
            }
            /*
            // Numérotation de chaque valeur 1 .. n
            axeX.min = courbe.at(0).x
            axeX.max = courbe.at(courbe.count-1).x
            axeX.tickCount = axeX.max*/
            // Ou affichage de l'horodatage
            horodatage.setTime(Math.min.apply(null, horodatages))
            axeX.min = horodatage
            horodatage.setTime(Math.max.apply(null, horodatages))
            axeX.max = horodatage
            axeX.tickCount = horodatages.length
            axeY.min = Math.min.apply(null, temperatures)
            axeY.max = Math.max.apply(null, temperatures)
        }
        ...
    }

    ChartView {
        ...
    }
}

Il existe une autre approche pour construire des courbes en faisant interagir C++/QML dans l’exemple qtcharts-qmloscilloscope-example.

Capture d’écran

On obtient :

Code source

Lien : MonApplicationBDCharts.zip

Évolution de la GUI

Dans cette version 2, on choisit de pouvoir passer de la vue “liste” à la vue “graphique” par un simple balayage sur l’écran. On retire donc le bouton radio.

On regroupe les deux affichages dans un ListView que l’on paramètre pour un balayage horizontal dans Releves.qml. On ajoutera un PageIndicator pour informer l’utilisateur.

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3

Page {
    width: parent.width
    padding: 10
    property int nbMesures: 5
    
    ...
    Row {
            ListView {
                id: resultats
                visible: false
                width: colonnePage.width
                height: Screen.desktopAvailableHeight * 0.45
                focus: true
                snapMode: ListView.SnapOneItem
                highlightRangeMode: ListView.StrictlyEnforceRange
                highlightMoveDuration: 250
                orientation: ListView.Horizontal
                boundsBehavior: Flickable.StopAtBounds

                model: ListModel {
                    ListElement {component: "Mesures.qml"}
                    ListElement {component: "Graphique.qml"}
                }

                delegate: Loader {
                    width: resultats.width
                    height: resultats.height
                    source: component
                    asynchronous: true
                }
            }
    }
    Row {
            anchors.horizontalCenter: parent.horizontalCenter
            PageIndicator {
                id: indicateur
                visible: false
                count: resultats.count
                currentIndex: resultats.currentIndex
            }
    }
    ...
}

Lien : MonApplicationBDChartsv2.zip

Voir aussi

Qt pour Android :

http://tvaira.free.fr/