Tâches de fond(12 heures)

Pour ces exercices, vous créerez un dépôt git nommé ImageLibrary.

Démarrage

L'objectif sera ici de développer une application capable d'énumérer récursivement les images contenues dans un répertoire choisi par l'utilisateur.

Pour l'instant, notre classe ImageLibrary aura trois champs privés :

Le premier représentera les données à visualiser, le second sera le widget de visualisation.

Le troisième sera tout simplement la barre d'outils de notre application.

Pour organiser notre application, nous allons nous baser sur le widget QMainWindow.

Comme indiqué dans la description de cette classe, QMainWindow fournit les bases d'une application classique : barre de menu, barres d'outils et widget central.

Signaux et slots

Comme indiqué sur cette page, Qt repose sur le mécanisme des signaux et des slots.

Ce mécanisme permet de simplifier la communication entre les différents composants d'un programme : les événements utilisateurs génèrent l'émission de signaux qui auront préalablement été connectés à des slots cibles.

La programmation se trouve grandement simplifiée car les objets émetteurs n'ont pas besoin de connaître par avance les récepteurs, tout comme les récepteurs n'ont pas besoin de connaître les émetteurs.

En bref, les signaux permettent de faire transiter des données de l'émetteur au récepteur sans qu'ils se connaissent. La seule contrainte étant que la signature du slot soit identique à celle du signal.

Dans un premier temps, nous ajouterons à notre classe ImageLibrary une méthode nommée go.

Cette méthode sera chargée d'ouvrir une fenêtre modale qui permettra à l'utilisateur de sélectionner le répertoire de son choix puis d'informer en retour l'utilisateur de sa décision.

À cet effet, nous ferons d'abord appel à la méthode statique getExistingDirectory de la classe QFileDialog. Selon la réponse obtenue, nous afficherons soit un simple message d'information indiquant le répertoire désiré, soit un avertissement dû, par exemple, à un abandon de l'utilisateur.

Dans un second temps, nous connecterons la méthode nouvellement codée à l'action "GO !".

Cette connexion pourrait être réalisée en deux temps :

  1. ajout d'une QAction via la version la plus simple de QToolBar::addAction,
  2. connexion du signal QAction::triggered à la méthode go de l'instance this.

Néanmoins, nous préfererons la version deux-en-un proposée par QToolBar::addAction.

Persistance des paramètres

Pour l'instant, votre application ne conserve aucune trace des choix de l'utilisateur.

Si l'utilisateur souhaite sélectionner le même répertoire plusieurs fois d'affilée, il est actuellement contraint de répéter la même séquence d'actions pour atteindre le répertoire de son choix.

Qt offre pourtant une solution élégante pour gérer des paramètres persistants, fonctionnalité pratique et très appréciée des utilisateurs.

Quelle que soit la plateforme utilisée (Linux, macOS, Windows, etc.), la classe QSettings permet de conserver des paramètres persistants en les identifiant par de simples clés textuelles : la méthode QSettings::setValue permet d'enregister la valeur d'un paramètre, tandis que la méthode QSettings::value permet de la récupérer. Cette dernière retourne le résultat sous la forme d'un QVariant, classe capable d'encapsuler des données de natures hétérogènes : booléens, entiers, chaînes de caractères, etc. Ce mécanisme nécessite que les propriétés organizationName et applicationName aient été définies au préalable.

Worker

Maintenant que l'utilisateur peut sélectionner un répertoire de départ, nous pouvons désormais lister le contenu dudit répertoire de façon récursive.

Pour effectuer cette tâche, nous proposerons une classe dédiée nommée Worker.

Cette classe sera déclarée dans ImageLibrary.h et implémentée dans ImageLibrary.cpp.

Son principe de fonctionnement sera assez simple : elle disposera d'un unique champ path initialisé à la construction et d'une unique méthode process qui analysera le contenu de path.

Basée sur une file d'attente, cette méthode traitera chaque élément de la file jusqu'à épuisement : s'il s'agit d'un répertoire, elle ajoutera en fin de file ses fichiers et sous-répertoires ; sinon, elle émettra le signal newItem en précisant le chemin du fichier.

En guise de file d'attente, nous opterons pour une QStringList.

En outre, les classes QFileInfo et QDir permettront de parcourir le système de fichiers.

Seuls les répertoires et les fichiers d'extensions png, jpg et jpeg seront retenus.

Enfin, le worker devant être capable d'émettre ses propres signaux, il héritera de la classe QObject.

Worker asynchrone

La solution proposée, bien que fonctionnelle, est par nature bloquante.

C'est à dire que le worker monopolise la boucle d'événements du programme et interdit toute interaction tant que la méthode process n'a pas abouti.

Au sein même de Qt, différentes solutions coexistent.

Parmi elles, une approche de bas niveau consiste à déclarer un gestionnaire de thread de type QThread qui permettra de dissocier une tâche du thread principal.

Le cycle de vie du worker ainsi rattaché à un autre thread devra alors avoir été préalablement planifié à travers le mécanisme de signaux et de slots comme décrit sur cette page :

QThread * thread = new QThread;
Worker * worker = new Worker (path);
worker->moveToThread (thread);
connect (thread, &QThread::started, worker, &Worker::process);
connect (worker, &Worker::finished, thread, &QThread::quit);
connect (worker, &Worker::finished, worker, &Worker::deleteLater);
connect (thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start ();

En complément de l'approche bas niveau, Qt propose le framework QtConcurrent.

Miniatures

Désormais, il ne nous reste plus qu'à créer des miniatures !

Cette dernière étape nécessite une refonte totale de notre modèle de données car nous ne nous contenterons plus du simple chemin de chaque image : nous allons maintenant lui associer une miniature.

Nous déclarerons donc une classe Item qui sera constituée de deux champs publics :

Cette classe exposera un unique constructeur Item (const QString &, const QImage &).

Nous pourrons ensuite déclarer une classe Model dérivée de QAbstractListModel dont l'unique attribut, items, sera de type QList<Item>.

En tant que classe dérivée de QAbstractListModel, la classe Model devra surcharger les méthodes rowCount et data : la première sera chargée d'indiquer le nombres d'éléments présents ; la seconde devra retourner les différentes données d'un élément à travers un QVariant.

En l'occurence, thumbnail servira d'icône et path d'infobulle.

Enfin, pour que notre modèle soit utilisable, n'oublions pas de lui ajouter un slot Model::addItem qui acceptera deux arguments : un chemin et la miniature correspondante.

Ce slot sera le point d'entrée pour ajouter de nouveaux éléments.

Comme indiqué dans la documentation, il sera impératif d'avertir les vues connectées au modèle lors de la modification des données. Cela se traduira simplement par un appel à beginInsertRows avant l'insertion de nouveaux items puis un appel à endInsertRows une fois la modification effectuée.

Il ne nous reste plus qu'à générérer lesdites miniatures.

Dans cette optique nous ajouterons à la classe Worker une méthode statique nommée Thumbnail.

Cette méthode acceptera le chemin d'une image comme argument et sera chargée de la lire à travers la classe QImage puis de la redimensionner avec QImage::scaled.

Nous déclarerons une macro THUMBNAIL_SIZE, ayant pour valeur 128, qui vous servira à la fois de hauteur et de largeur maximales de la miniature.

Nous prendrons particulièrement soin de conserver les proportions initiales de chaque image.

Capture d'écran
Exemple de résultat attendu

Miniatures asynchrones

La génération des miniatures pourra également devenir asynchrone pour peu que l'on gère la communication entre threads.

Nous ajouterons chaque chemin à une liste qui sera traitée en fin de process de façon parallèle via QtConcurrent::mapped. Une méthode statique Worker::MappedItem servira ici à transformer chaque chemin en item.

Nous mettrons en place une surveillance du QFuture retourné à l'aide d'un QFutureWatcher puis nous implémenterons un slot Worker::processItem (int k) qui émettra le signal Worker::newItem dès qu'un résultat sera annoncé par QFutureWatcher::resultReadyAt.

Par contre, en l'absence de boucle de gestion des événements au sein du worker, les signaux qu'il recevra seront mis en attente (Qt::QueuedConnection) et ne seront jamais traités !

Deux solutions existent pour pallier cet inconvénient majeur :

  1. Revenir au schéma moveToThread vu précédemment et se reposer sur la boucle du QThread.
  2. Déclarer une boucle locale de type QEventLoop en fin de worker, connecter le signal QFutureWatcher::finished sur QEventLoop::quit puis la démarrer avec QEventLoop::exec.

Nous préférerons évidemment la première solution, plus simple et plus élégante.

Cette approche introduit néanmoins un défaut inattendu : l'ordre des images peut désormais être perturbé par le temps de génération des miniatures.

Finalement, plutôt que d'attendre que sa miniature ait été générée, nous ajouterons chaque item immédiatement. Cela implique logiquement de modifier la méthode Model::addItem ainsi que la signature de Worker::newItem.

Surtout, nous devrons proposer un nouveau signal Worker::newThumbnail et un nouveau slot Model::setThumbnail dont l'objet sera de mettre à jour la miniature d'un item identifié par son indice.


Exercices complémentaires

HTML5