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.
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();
}
Dans l’exemple vu précédemment, on a créé une base de données mabase.sqlite
comprenant 2 tables :
releves
qui contiendra la description des relevésmesures
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
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/
}
}
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 :
double
)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’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.
On obtient :
Lien : MonApplicationBDCharts.zip
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
Qt pour Android :