Blog ENI : Toute la veille numérique !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
💥 Du 22 au 24 novembre : Accès 100% GRATUIT
à la Bibliothèque Numérique ENI. Je m'inscris !
  1. Livres et vidéos
  2. Maîtrisez Qt
  3. Qt Core
Extrait - Maîtrisez Qt Guide de développement d'applications professionnelles (3e édition)
Extraits du livre
Maîtrisez Qt Guide de développement d'applications professionnelles (3e édition)
1 avis
Revenir à la page d'achat du livre

Qt Core

Objectifs

Dans ce chapitre nous vous présenterons le module principal, le plus important de Qt : le module Qt Core. Celui-ci contient un certain nombre de classes indispensables pour la création d’applications, telles que QString pour les chaînes de caractères, QTimer pour la programmation d’événements, QFile et QDir pour la gestion des fichiers et encore bien d’autres.

Ce module est un des plus importants, car sur lui reposent tous les autres modules. Il intègre une grande partie des fonctionnalités offertes par Qt et décrites dans le chapitre Les fondations de Qt, en particulier la classe QObject.

Intégration et interactions

Le module Qt Core est intégré à votre projet grâce à l’ajout du mot-clé core dans la déclaration QT de votre fichier .pro.

QT += core 

Il est pratiquement impossible de s’en passer, sauf si vous faites de la programmation purement C++ sans utiliser aucune des fonctionnalités de Qt. N’importe quel autre module de Qt aura besoin de celui-ci.

Découverte de Qt Core

Qt Core est un ensemble d’API qui fournit essentiellement des types de base comme les chaînes de caractères Unicode, les types fichier et répertoire pour l’accès aux disques, des interfaces pour les entrées-sorties et des mécanismes de gestion des événements.

Cet ensemble de classes est nommé, dans l’univers Qt, un module. Il contient notamment la classe QObject, la mère des classes contenues dans les autres modules de Qt et de vos futures classes. En outre, la plupart des classes de Qt Core sont utilisées par les autres API de Qt.

1. Le méta-objet

Comme expliqué dans le chapitre Anatomie d’une application, Qt utilise un système de méta-objets pour faciliter le travail du développeur dans la gestion des signaux et l’appel asynchrone des fonctions.

La méta-classe correspondant au méta-objet est directement issue de l’héritage de la classe QObject et de l’usage de la macro Q_OBJECT.

Lorsque vous utilisez la macro Q_OBJECT, vous ajoutez plusieurs déclarations de fonctions à votre classe. Ces fonctions seront utilisées par Qt pour réaliser les appels de slots et générer les signaux.

Comme vous n’implantez pas vous-même ces fonctions surnuméraires, c’est Qt qui va le faire pendant la phase de construction du programme. L’utilitaire...

Créer et piloter des threads

La gestion des threads est un point extrêmement délicat, surtout lorsque l’on souhaite que notre application soit multiplateforme.

Il existe principalement deux types de gestion des threads très différents : POSIX et WIN32. Le premier est une implantation Unix et le second existe uniquement sous Windows.

Pour satisfaire l’interopérabilité des programmes, Qt propose une classe de gestion des threads : QThread. Attention toutefois, il ne s’agit pas d’un thread à proprement parler, mais davantage d’un contrôleur pour un thread système.

1. Paralléliser des tâches

Il faut être très prudent lorsque l’on décide de créer des threads. La plupart du temps, un développeur peu expérimenté créera un ou plusieurs threads pour résoudre un problème de performances ou de blocage de l’interface graphique. C’est une mauvaise interprétation du problème et une solution inadaptée, le blocage de l’interface graphique est révélateur d’un problème conceptuel (voir chapitre Les fondations de Qt).

Avec Qt, il y a très peu besoin de créer des threads, et surtout pas pour résoudre ce genre de problèmes. Les threads doivent être utilisés uniquement pour paralléliser des sous-processus. Par exemple, le parcours d’un arbre avec un algorithme de graphes peut être parallélisé avec un ou plusieurs threads.

S’il s’agit simplement d’un long travail à effectuer, sans parallélisme, il faudra utiliser la run-loop du thread principal et les signaux/slots pour intégrer ce travail de manière fluide dans le flux de l’application.

Il existe plusieurs façons de créer et de gérer des threads : les threads pilotés dont on souhaite connaître l’état et que l’on souhaite pouvoir démarrer et arrêter de manière maîtrisée ainsi que les tâches autonomes que l’on exécute et qui se terminent « dans l’anonymat ».

a. Les threads pilotés

La classe QThread propose un mécanisme d’encapsulation d’une tâche à exécuter dans...

Les structures de données

Les structures de données sont parmi les classes les plus utilisées dans les programmes. Un développeur a presque toujours besoin de stocker des données en mémoire, en les indexant pour les retrouver facilement plus tard. Qt fournit plusieurs structures permettant de gérer des listes, des couples clé-valeur, des matrices, etc.

Ces structures de données ont été conçues pour être très performantes, tant dans l’écriture que dans la lecture ou la copie. Elles utilisent le partage implicite, sont réentrantes et thread-safe.

Les structures de données de Qt utilisent le mécanisme du partage implicite, il est donc recommandé de les instancier automatiquement et de les transmettre par valeur et non par référence.

La documentation de Qt explique, pour chaque classe, quelle est sa structure interne, ses performances en lecture, en écriture et, le cas échéant, en parcours.

1. Structures unidimensionnelles

Parmi les structures les plus courantes se trouvent les tableaux. En C, un tableau est déclaré à l’aide des crochets []. L’espace mémoire est alors réservé statiquement à la compilation dans l’espace de données privé du programme.

Le développeur est obligé de définir une dimension pour son tableau dans le code ou lors de son instanciation et ce tableau est alors immuable. Si l’on souhaite l’agrandir, il faut en créer un autre et copier les données de l’un dans l’autre. La recherche est très complexe et les performances faibles.

Qt fournit aux développeurs plusieurs classes qui utilisent le modèle du partage implicite et du copy-on-write (voir chapitre Les fondations de Qt).

Les données ne sont pas stockées directement dans l’objet que vous manipulez mais uniquement le pointeur, les données sont copiées uniquement lorsque c’est nécessaire (en utilisant la fonction takeAt(int i) par exemple, qui renvoie une copie de l’objet situé à l’index i et le supprime de la liste).

Ce qui fait que la taille des données à stocker est celle d’un pointeur et que les performances en écriture sont linéaires...

QByteArray

Avec Qt il est préférable de ne pas créer de tableaux d’octets comme en C. Les syntaxes suivantes sont donc à éviter :

char tableau[] = {1,2,3,4}; 
char *tableau2 = new char[20]; 
memset(tableau2, 0, 20); 
delete[] tableau2; 
delete tableau2; 

Les raisons sont les mêmes que celles que nous avons exposées dans le chapitre Les fondations de Qt. L’asynchronisme de l’exécution des fonctions fait que vous ne pouvez pas savoir avec certitude quand sera le bon moment pour détruire votre tableau.

Qt fournit la classe QByteArray pour gérer les tableaux d’octets. Cette classe permet de gérer très simplement et efficacement les données brutes. Elle utilise la copie à l’écriture, ce qui évite la copie des données lors du passage par valeur.

Dans les API de Qt, tous les échanges de données se font avec cette classe. La classe QString reçoit des chaînes et les exporte grâce à la classe QByteArray. Les sockets réseau stockent les données reçues dans un QByteArray. Tout comme la classe QFile qui sérialise et désérialise dans les fichiers.

Les données stockées dans une instance de QByteArray sont des données brutes, cela inclut tous les caractères non imprimables et les \0.

La classe QByteArray est conçue selon le patron de conception Proxy, les données sont donc séparées...

QBuffer

La classe QBuffer est une interface de type entrée/sortie vers une instance de QByteArray. Elle permet d’accéder au contenu d’un QByteArray tel que vous le feriez pour un fichier binaire.

Elle hérite de la classe QIODevice, au même titre que la classe QFile, leur utilisation est donc identique.

Cette classe permet de sérialiser en mémoire des instances et de les désérialiser plus tard, cela peut-être utile dans le cadre du presse-papier par exemple.

QByteArray data; 
QBuffer buffer(&data); 
 
//Ouverture du buffer en écriture 
buffer.open(QIODevice::WriteOnly); 
 
//Ouverture d'un canal de données brutes vers le buffer 
QDataStream sortie(&buffer); 
sortie << monObjetASerialiser; 

Les fonctions suivantes doivent être implantées pour permettre la sérialisation de n’importe quelle classe :

QDataStream &operator<<(QDataStream&, const VotreClasse&);  
QDataStream &operator>>(QDataStream&, VotreClasse&); 

Ces fonctions sont globales, elles ne doivent pas être implantées dans une classe.

Pour en savoir plus sur la sérialisation des objets, reportez-vous au chapitre Qt Core - La sérialisation.

QString

Chaque kit de développement fournit sa classe ou son type permettant de gérer les chaînes de caractères. La STL fournit std::string et std::wstring qui sont respectivement des tableaux d’octets et d’entiers sur 16 bits. Ils sont très difficiles à utiliser pour stocker des chaînes encodées en UTF-8.

Qt fournit deux mécanismes complémentaires d’une puissance et d’une souplesse incomparables : QString et QByteArray.

1. Les chaînes C et les langues accentuées

Une chaîne de caractères en C est un pointeur sur un tableau de char, elle s’écrit char* maChaine et son adresse peut être obtenue en lisant l’adresse du premier caractère : char* laChaine = &maChaine[0]. Elle est délimitée par la valeur 0 située en fin de chaîne. Cette valeur terminale n’appartient pas à la chaîne. Une des principales caractéristiques des chaînes C est donc que les données sont contiguës en mémoire.

Ceci étant dit, le problème du jeu de caractères se pose tout de suite : en Latin1 ou ASCII les caractères sont codés sur un octet, un unsigned char convient donc parfaitement pour coder un caractère ASCII.

images/06SC10N.png

Cependant, la table ASCII ne contient pas les caractères des alphabets cyrillique, arabe ou hébreu, pas même le symbole Euro. Il n’est donc pas possible de coder dans le jeu de caractères ASCII le symbole Euro, ni de créer une interface graphique en langue suédoise.

2. Les normes ISO, Unicode, UCS

Pour remédier à ce problème, plusieurs normes ont été créées pour coder tous les caractères qui doivent l’être pour couvrir tous les langages connus. Le standard Unicode contient à ce jour plus de 100 000 caractères, couvre plus de 90 écritures et symboles et est capable de gérer le sens de l’écriture (droite à gauche aussi bien que gauche à droite).

Coder plus de 100 000 caractères dans une table nécessite bien plus qu’un unsigned char qui code au maximum 255 valeurs. Les nouvelles normes utilisent maintenant une taille allant de 1 à 4 octets pour coder un caractère.

Nous n’entrerons...

Interactions avec le système d’exploitation

Interagir avec le système d’exploitation suppose de différencier les commandes à exécuter en fonction du système pour lequel le programme est compilé.

Dans la plupart des cas, Qt simplifie le travail du développeur en lui évitant, par exemple, d’avoir à différencier les séparateurs dans les noms de fichiers (/sous Unix, \ sous Windows).

Toutefois, si dans votre cas, la différenciation était toujours nécessaire, elle se ferait à l’aide des directives Q_OS_* du précompilateur parmi lesquelles figurent notamment Q_OS_MAC, Q_OS_WIN et Q_OS_LINUX. Un simple #if defined(Q_OS_WIN) permettra de créer du code contextuel pour Windows.

1. Exécuter une commande externe

Dans de rares cas, il est indispensable d’exécuter une commande externe à l’application, puisée directement dans les exécutables du système d’exploitation.

Par exemple, si l’utilisateur souhaite « pinger » une machine du réseau, il ne peut pas le faire simplement avec les API de Qt, et en tout état de cause une telle requête réseau nécessite les droits d’administrateur pour être exécutée.

L’exécution d’un processus externe est très simple avec la classe QProcess. Comme l’exécution d’une commande externe est par nature aléatoire, Qt rend cette exécution asynchrone par défaut.

L’exécution d’une commande externe s’effectue comme ceci :...

La gestion des paramètres

Dans une application, on a parfois besoin de mémoriser les paramètres de l’application, les positions et dimensions des fenêtres et certains détails sur l’utilisation du logiciel.

Ces informations peuvent être stockées de différentes manières en fonction de la simplicité recherchée, de la portée des données enregistrées et de leur caractère confidentiel.

1. QSettings

Qt offre un moyen très simple de gérer les réglages d’une application : la classe QSettings. Celle-ci fonctionne sur tous les systèmes d’exploitation mais n’offre pas de moyen de transférer les paramètres d’un système à un autre.

Sous Windows, c’est la base de registres qui sera utilisée par défaut. Sous OS X c’est un fichier XML stocké dans la sandbox et sous Unix c’est un fichier .ini.

Ces formats de stockage par défaut peuvent être outrepassés par le développeur pour enregistrer les réglages dans un fichier XML stocké à un emplacement qu’il aura lui-même défini.

Avant de pouvoir utiliser correctement la classe QSettings, le développeur devra prendre soin de « paramétrer » son application. Trois paramètres sont importants :

QCoreApplication::setOrganisationName("MonEntreprise") ; ...

La sérialisation

La sérialisation est un mécanisme qui permet d’enregistrer l’état d’une instance d’objet. Cela s’appelle de la persistance de données. Généralement la sérialisation d’un objet se fait dans un fichier XML ou dans une base de données. Grâce à ce mécanisme, il est possible de lire les données d’un objet sérialisé pour le recréer à l’identique ultérieurement.

Ce mécanisme est très utile pour enregistrer des données à partir d’objets complexes. Un programme ne manipule pas toujours des fiches contenant des noms, prénoms et adresses dont les données trouveront facilement leur place dans une table SQL contenant des champs de texte.

Par exemple un jeu devra enregistrer l’état de tous les objets qu’il a créé depuis le démarrage (personnages, scènes, objets ramassés, etc.).

Ces objets devront être, pour certains, enregistrés dans des champs de texte, mais les plus complexes le seront, dans des champs de données binaires, du type BLOB.

1. Mécanismes

Pour sérialiser un objet avec Qt, il suffit d’utiliser la classe QTextStream si l’on souhaite l’enregistrer sous forme de texte, ou la classe QDataStream si l’on souhaite enregistrer des données binaires. Le second est plus...

Les fichiers XML

La lecture et l’écriture de fichiers XML sont effectuées à l’aide d’une classe dédiée au format XML. Avec Qt, la manipulation de fichiers XML se fait très simplement à l’aide d’un parser SAX intégré au module Qt Core.

Ces classes remplacent les mécanismes de l’ancien module QtXml aujourd’hui déprécié.

Les classes chargées de la manipulation des fichiers XML sont QXmlStreamWriter et QXmlStreamReader. Elles sont instanciées avec, comme paramètre un objet QIODevice (fichier ou socket réseau par exemple), QString ou QByteArray. Quel que soit le type de stockage des données que vous choisirez, les fonctions fournies seront utilisées de la même manière.

1. Écrire dans un fichier XML

La classe QXmlStreamWriter fonctionne de manière événementielle, en parcourant le flux de données (le fichier XML) du début à la fin de façon séquentielle. Ainsi, les balises sont lues dans l’ordre dans lequel elles figurent dans le fichier, ce qui implique qu’elles soient aussi écrites dans l’ordre dans lequel vous les fournissez à la classe.

La création d’un document commencera toujours par l’appel à la fonction writeStartDocument() et se terminera par un l’appel à la fonction writeEndDocument(). Qt se chargera alors d’écrire les balises déclaratives adéquates.

Ensuite tous les éléments sont ajoutés grâce à la fonction writeStartElement(QString name) qui prend en paramètre le nom de l’élément à ajouter.

Pour ajouter des attributs à l’élément courant (celui qui est actuellement ouvert), nous utiliserons la fonction writeAttribute(QString name, QString...

Les fichiers JSON

Le format de fichier JSON est de plus en plus utilisé dans l’échange de données entre applications. Sa structure est indentée et claire, ce qui le rend plus facilement lisible qu’un fichier XML.

En revanche il n’offre pas de possibilité de typer les champs qu’il contient et ne possède pas de mécanisme de validation des données.

1. Lire un fichier JSON

L’interface entre un fichier et une structure de données JSON est la classe QJsonDocument

Pour ouvrir le fichier stocké sur le disque dur :

QFile fichier(":/fichier.json"); 
 
//Ouverture du fichier en lecture uniquement 
if(fichier.open(QFile::ReadOnly)) { 
   //Récupération du contenu du fichier 
   donnees = fichier.readAll(); 
 
   //Interprétation du fichier JSON 
   QJsonDocument doc = QJsonDocument::fromJson(donnees, &error); 
   if(error.error != QJsonParseError::NoError) { 
       qCritical() << "Impossible d'interpréter le fichier : 
" << error.errorString(); 
   } else { 
       qDebug() << "Fichier interprété"; 
   } 
}...

Automate à états

Qt dispose d’un framework permettant de gérer les automates. Celui-ci se compose de plusieurs classes qui vous permettront de modéliser et gérer facilement des états et des transitions.

1. Automate simple

Supposons un feu tricolore. Celui-ci a trois états possibles :

  • Feu au vert

  • Feu à l’orange

  • Feu au rouge

Les transitions entre ces trois états sont réalisées après un décompte de deux secondes à chaque fois.

images/06SC01N.png

L’implémentation de cet automate en Qt s’effectuera ainsi :

//Instanciation d'un automate et des états 
QStateMachine automateFeux; 
QState etatFeuVert; 
QState etatFeuOrange; 
QState etatFeuRouge; 
 
//L'événement déclencheur des transitions 
QTimer timer; 
 
//Ajout des états à l'automate 
automateFeux.addState(&etatFeuVert); 
automateFeux.addState(&etatFeuOrange); 
automateFeux.addState(&etatFeuRouge); 
 
//Définition de l'état initial de l'automate 
automateFeux.setInitialState(&etatFeuRouge); 
 
//Définition des transitions 
etatFeuRouge.addTransition(&timer, SIGNAL(timeout()), &etatFeuVert); 
etatFeuVert.addTransition(&timer, SIGNAL(timeout()), &etatFeuOrange); 
etatFeuOrange.addTransition(&timer, SIGNAL(timeout()), &etatFeuRouge); 

Nous disposons à présent d’un automate à trois états. Un unique timer déclenche les transitions entre états. C’est le signal timeout() du timer qui déclenche la transition.

Une transition est définie comme ceci :

etatSource.addTransition(objetDéclencheur, signalDéclencheur, 
etatDestination); 

Pour démarrer au automate, il suffit d’appeler la fonction start().

automateFeux.start(); 

Un automate possède sa propre event-loop. Si la fonction start() n’est pas appelée, il ne pourra recevoir de signaux ni effectuer les transitions.

Après avoir démarré l’automate, nous devons...

Fonctions et types globaux

Qt possède un certain nombre de fonctions et de types globaux, utilisables depuis n’importe quel endroit du code. Nous allons en faire un petit tour d’horizon.

1. Conversion d’endianisme

Il est parfois nécessaire de changer l’endianisme de données en provenance d’une autre architecture, notamment si vous échangez des données brutes sur un réseau.

Pour pouvoir utiliser les fonctions de conversion, vous devez inclure le fichier QtEndian :

#include <QtEndian> 

La conversion d’un entier vers gros-boutiste se fait ainsi :

quint32 a = 0x00112233; 
quint32 a2 = qToBigEndian(a); 
quint16 b = 0x00FF; 
quint16 b2 = qToBigEndian(b); 
 
qDebug() << "int32 little endian (x86) = " << QString::number(a, 16); 
qDebug() << "int32 big endian (x86) = " << QString::number(a2, 16); 
 
qDebug() << "int16 little endian (x86) = " << QString::number(b, 16); 
qDebug() << "int16 big endian (x86) = " << QString::number(b2, 16); 

Cela produit le résultat suivant :

quint32 little endian (x86) = "112233" 
quint32 big endian (x86) = "33221100" 
quint16 little endian (x86) = "ff" 
quint16 big endian (x86) = "ff00" 

Bien entendu, ce résultat ne vaut que si le code est exécuté sur une architecture petit-boutiste. Dans le cas contraire, la fonction qToBigEndian() renverra la valeur d’entrée.

La conversion d’un entier depuis gros-boutiste se fait ainsi :

quint32 a = 0x33221100; 
quint32 a2 = qFromBigEndian(a); 
quint16 b = 0xff00; 
quint16 b2 = qFromBigEndian(b); 
 
qDebug() << "quint32 big endian (x86) = " << QString::number(a, 16); 
qDebug() << "quint32 little endian (x86) = " << QString::number(a2, 16); 
 
qDebug() << "quint16 big endian (x86) = " << QString::number(b, 16); 
qDebug() << "quint16 little endian (x86) = " << QString::number(b2, 16); 

Cela produira le résultat suivant :...