Les fondations de Qt
Objectifs
Dans ce chapitre, nous souhaitons répondre aux questions qui nous sont immanquablement posées lors de nos séminaires de formation : quelle est la « philosophie » de Qt et comment est-il structuré ?
La réponse à ces questions permet de bien comprendre l’organisation interne de Qt et de s’en inspirer pour créer ses propres applications, mais surtout de l’utiliser de la meilleure façon possible.
Les Types Qt
La notion de Type est elle aussi assez spécifique à Qt. Elle traverse tout le framework pour servir, notamment, à la construction d’interfaces graphiques Qt Quick. Les objets déclarés dans une scène Qt Quick sont des Types QML. Chaque Type QML s’appuyant indirectement sur une classe C++, les caractéristiques d’un Type Qt sont logiquement à rechercher dans les classes C++.
La définition d’un Type Qt répond aux exigences imposées par la classe QMetaType suivantes :
-
Un constructeur public par défaut, c’est-à-dire sans argument.
-
Un constructeur de copie public.
-
Un opérateur d’assignation.
-
Un destructeur public, ce dernier n’a pas besoin d’être déclaré explicitement, mais c’est une bonne pratique, et s’il est déclaré, il doit être virtuel.
L’exemple ci-dessous répond à ces exigences et peut être considéré comme un Type Qt :
class MonType {
public :
MonType() = default;
virtual ~MonType() = default;
MonType(const MonType&) = default;
MonType& operator=(const MonType&) = default;
};
Cette déclaration n’est que le minimum exigé pour que la classe soit considérée comme un Type Qt. Vous...
La classe QObject
La classe QObject est au centre de toutes les API de Qt. Elle est la mère de pratiquement toutes les classes, hormis les conteneurs de données et certaines classes aux propriétés spécifiques.
Elle possède des caractéristiques particulières qui en font une classe incontournable quand on souhaite bénéficier des possibilités offertes par Qt, mais elle impose aussi certaines contraintes, faisant ainsi de Qt un framework.
1. Entité et identité
Une des premières choses qui étonne lorsqu’on commence à développer avec Qt est le fait qu’une classe qui hérite de QObject ne peut être copiée.
Le code suivant ne compilera pas :
class MaClasse : public QObject {
...
}
int main(int argc, char** argv) {
MaClasse a ;
MaClasse b(&a) ; //Cette ligne ne compile pas
MaClasse c = a ; //Celle-ci non plus
}
En effet, le constructeur de copie et l’opérateur d’assignation de la classe QObject sont désactivés.
La raison en est que les concepteurs de Qt ont souhaité que les objets instanciés dans chaque application soient considérés non pas comme des entités, c’est-à-dire des clones dont les propriétés sont identiques après la copie, ce qui est le cas par défaut en C++, mais comme des identités, dont les propriétés, sont potentiellement différentes d’un objet à l’autre.
Une identité n’est pas un simple clone, une copie d’un objet possède par nature une différence avec l’original, éventuellement un identifiant différent. En tout état de cause, la décision revient au concepteur de l’application, celui-ci pouvant choisir finalement de créer des clones.
Lorsque, dans votre système d’exploitation, vous effectuez un copier-coller d’un fichier MonFichier.txt dans le même répertoire, il est impossible de l’écraser, le système lui donnera obligatoirement un nom différent : MonFichier (1).txt ou Copie de MonFichier.txt. C’est le principe de l’identité....
Héritage
Qt s’appuie totalement sur C++, à ce titre toutes les règles d’héritage de C++ s’appliquent.
Comme nous l’avons vu ci-dessus, la spécialisation de la classe QObject impose un certain nombre de contraintes, en particulier la prise en charge de la copie des données et la gestion de l’affinité de thread.
1. Généralités
Lorsque vous concevez votre propre modèle objet, vous devrez vous interroger sur l’opportunité, pour chaque classe, d’hériter de la classe QObject. Pour savoir si vous devez hériter de cette classe, demandez-vous si vous souhaitez bénéficier d’une des fonctionnalités de cette classe :
-
Envoi et réception de signaux
-
Exécution de fonctions différées
-
Délégation de la destruction d’un objet
-
Traduction
-
Propriétés dynamiques
-
Timers
En règle générale, ces fonctionnalités sont nécessaires dans les contrôleurs, mais peu dans le reste d’une application.
Pour vous aider à faire vos choix lors de la conception de votre modèle objet, nous vous proposons un tableau récapitulatif des bonnes pratiques :
Type d’objet |
Type d’appel |
Héritage |
Objet du modèle |
Synchrone |
Aucun (1) |
Helper |
Synchrone |
Aucun, Statique |
DAO |
Synchrone |
Aucun, Singleton |
Factory |
Synchrone |
Aucun, Statique |
Contrôleur |
Asynchrone |
QObject |
Widget graphique |
Synchrone/Asynchrone |
QWidget -> QObject |
(1) Voir la section Optimisation des conteneurs dans ce chapitre
2. Prototype
Hériter de la classe QObject impose un certain formalisme.
Voici le prototype classique d’une...
Optimisation des conteneurs
Au sein de Qt, tous les conteneurs de données ainsi que de très nombreuses classes qui encapsulent un tant soit peu de données, sont conçus sur le modèle de la copie à l’écriture.
La copie à l’écriture est une technique couramment utilisée en conception objet, car elle permet d’optimiser à la fois la consommation de mémoire et l’utilisation du processeur.
Cette technique repose sur deux principes : le comptage de références et la séparation entre les données d’une classe et son interface (patron de conception Proxy). Le premier permet de gérer automatiquement le cycle de vie de l’instance créée et de passer les objets par valeur, et le second permet de ne copier les données que lorsque c’est nécessaire.
Nous verrons, dans les chapitres suivants, ces techniques fondamentales sur lesquelles sont fondées la plupart des structures de données de Qt.
Le patron de conception principal sur lequel est fondé ce mécanisme est le Proxy.
Outre le fait que tous les conteneurs de données de Qt sont optimisés, cette technique offre des gains de performance extraordinaires. En effet, elle permet de passer des arguments de fonctions par valeur, sans jamais copier les données.
Observons l’exemple suivant :
1 void fonction(QString str) {
2 QString str2 = str;
3
4 if(<condition>) {
5 return str2;
6 }
7
8 ...
9 }
10
11 void main() {
12 QString str("Ceci est une chaine de caractères");
13
14 fonction(str);
15 }
En principe, avec un autre framework C++, la chaine de caractères créée à la ligne 13 est copiée 3 fois : aux lignes 1, 2 et 5. Le constructeur par défaut d’une classe effectue une copie profonde de ses membres.
Avec Qt, même si l’objet créé en ligne 13 est bien copié 3 fois, la chaîne de caractère contenue dans l’instance de QString, elle n’est jamais copiée....
La classe QVariant
La classe QVariant est une classe très utilisée dans certains modules de Qt, notamment ceux qui servent à faire des échanges de données entre applications comme Qt Sql ou l’ancien module Qt Xml dont les mécanismes sont à présent implantés dans Qt Core (voir la section Les fichiers XML dans le chapitre Qt Core).
La classe QVariant est utilisée pour le transtypage. QVariant n’est pas un type, C++ ne fournit pas de type variant. Cela n’est pas non plus équivalent au type auto de C++-11.
Dans le cas où vous lisez un fichier XML ou que vous parcourez les enregistrements résultant d’une requête SQL, vous ne traitez que des instances de la classe QVariant. Vous devez ensuite convertir ces variants en un type exploitable par votre application.
L’intérêt majeur de la classe QVariant est de permettre de manipuler des types différents, de les convertir sans erreur ni exception, et de n’avoir qu’un type à manipuler dans une API.
Par exemple, la conversion d’un booléen en chaîne de caractères et inversement sera faite ainsi :
QVariant var(false) ;
bool b = var.toBool() ;
qDebug() << "booléen : " << b ;
QString s = var.toString() ;
qDebug() << "chaine :...
Gestion des erreurs
Qt n’émet jamais d’exception, car il serait absurde de déclencher une exception durant un traitement asynchrone, nous n’aurions pas la possibilité d’attraper (catch) l’exception.
En revanche, dans les API de Qt vous trouverez principalement deux manières de gérer les erreurs : bool* ok = 0 et error().
1. bool *ok = 0
Ce principe est utilisé notamment dans la classe QVariant que nous avons vue plus haut. Le principe est que lorsque vous appelez une fonction qui doit renvoyer une valeur, Qt renvoie toujours une valeur, même si le traitement a échoué.
Par exemple, lorsque vous convertissez une chaîne en entier, il est possible qu’une erreur se produise si la chaîne n’est pas correctement formatée.
Un second argument optionnel du type bool* contient alors le résultat de la conversion.
QString val = "1a" ;
bool ok(true);
QVariant var(val) ;
int i = var.toInt(&ok) ;
if(!ok)
qDebug() << "Erreur de conversion" ;
Dans l’exemple ci-dessus, la fonction toInt() a échoué, car il n’a pas été possible de convertir la chaîne « 1a » en entier. Elle a donc retourné la valeur 0 et a positionné la variable ok à...
Le modèle event-driven
Parmi les notions que nous avons introduites précédemment pour décrire les paradigmes sur lesquels repose Qt en tant que framework, figurait le modèle event-driven.
Nous revenons sur cette notion dans ce chapitre afin de comprendre pourquoi elle est si importante et à quel point elle se situe au cœur de la conception de Qt, et donc de vos futures applications.
L’assimilation de ces notions est cruciale pour réaliser des applications performantes, qui consomment peu de ressources et dont le fonctionnement est fluide en toutes circonstances.
1. Pourquoi Qt est-il orienté événements ?
La conception d’une application, lorsqu’elle met en jeux des processus concurrents, des threads et une interface graphique, est d’une difficulté majeure. Surtout lorsque les threads dépendent les uns des autres et que le fonctionnement des différents threads peut conduire à mettre à jour l’interface graphique.
La gestion des threads dépend essentiellement de la plateforme de développement et de la bibliothèque système utilisées pour la création et la gestion des threads (voir le chapitre Qt Core - Créer et piloter des threads).
Qt masque complètement au développeur la complexité de la gestion des processus enfants, des threads et la communication entre ces différents éléments. Grâce à des API de haut niveau, le développeur peut modéliser son application et faire communiquer les instances de ses objets, quelle que soit leur nature.
En langage C, un appel de fonction est toujours synchrone. Cela signifie que l’exécution d’une fonction est « déviée » dans l’appel d’une autre fonction avant de revenir pour continuer son exécution.
Dans l’exemple suivant, l’exécution de la fonction main() est « déviée » à la ligne 8 dans la fonction b(), jusqu’à ce que celle-ci soit terminée et alors l’exécution de la fonction main() se poursuivra à la ligne 9.
1 void b() {
2 std::cout << "Ligne affichée en deuxième" << std::endl ; ...
Les ressources
Les applications modernes contiennent parfois du son et des polices de caractères spécifiques, mais surtout beaucoup d’images et d’icônes. Tous ces objets sont stockés dans des fichiers externes à l’application lorsque vous la développez.
Si vous conservez ces fichiers hors de votre application, il vous faudra les fournir en même temps que l’exécutable de votre programme si vous le diffusez. Cela peut rapidement rendre complexe la diffusion de vos logiciels ainsi que leur maintenance.
Pour éviter ces problèmes, Qt propose un système de ressources. Une ressource est un fichier source qui est compilé et lié dans l’exécutable de votre logiciel durant la phase de compilation.
Le fonctionnement est très simple :
-
Dans Qt Creator vous référencez les fichiers à intégrer dans l’exécutable.
-
Lors de la phase de compilation, qmake appelle l’outil externe qrc pour chacun des fichiers intégrés. Ces fichiers sont transformés en code source C, en l’occurrence des tableaux d’octets (voir chapitre Anatomie d’une application).
-
Chacun de ces fichiers sources générés est compilé et lié à votre exécutable.
Ainsi, votre exécutable sera plus gros, car il contiendra, en plus de votre code source, celui...
Les propriétés
L’intégration de C++ avec Quick, et en particulier le langage QML implique certaines contraintes dans le code des classes C++. Comme nous l’avons vu dans la section Les Types Qt, une classe est considérée comme un type Qt lorsqu’elle possède certaines caractéristiques, en particulier un constructeur par défaut.
QML est un langage déclaratif, il n’existe pas d’opération d’instanciation, ni de destruction. Pour créer un objet il suffit de déclarer le Type Qt et d’utiliser des accolades :
main.qml
UnType {}
Cette déclaration suffit à provoquer la création d’une instance de UnType, ce Type étant codé dans une classe C++.
Vous remarquerez également qu’il n’y a pas d’argument à une déclaration, comme on peut le voir avec un constructeur C++ qui permette de définir des variables membres en même temps que l’instanciation de la classe.
En QML, pour modifier des membres de la classe on utilise le plus souvent des propriétés :
main.qml
UnType {
id: letype
val: 1552
}
La déclaration ci-dessus est équivalente au code C++ suivant :
UnType* letype = new UnType(this);
letype->val = 1552;
Ceci suppose que le membre val soit public...