git
nommé ImageLibrary
.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.
ImageLibrary
.ImageLibrary
comme nom de classe en prenant soin de conserver
QMainWindow comme classe de base.Pour l'instant, notre classe ImageLibrary
aura trois champs privés :
model
de type QStringListModel ;view
de type QListView ;toolbar
de type QToolBar.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.
ImageLibrary
.model
à la vue view
via la méthode héritée
QAbstractItemView::setModel.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.
toolbar
:
toolbar.addAction ("GO !", qApp, &QApplication::aboutQt);
toolbar
via QMainWindow::addToolBar.view
comme widget central.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.
ImageLibrary.h
, déclarez la méthode go
.ImageLibrary.cpp
, implémentez la méthode go
à l'aide des méthodes statiques
QFileDialog::getExistingDirectory,
QMessageBox::information
et QMessageBox::warning.
En guise de point de départ, utilisez QDir::homePath.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 :
go
de l'instance this
.Néanmoins, nous préfererons la version deux-en-un proposée par QToolBar::addAction.
go
à l'action "GO !" en modifiant l'appel à
QToolBar::addAction./home/bib/aassif
.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.
main.cpp
, utilisez votre login comme paramètre de
setOrganizationName.ImageLibrary
,
avec setApplicationName.ImageLibrary::go
,
déclarez un objet settings
de classe QSettings
qui représentera la dernier répertoire choisi par l'utilsateur.
Sa valeur déterminera le point de départ de la fenêtre modale et elle sera mise à jour si l'utilisateur exprime un nouveau choix.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.
ImageLibrary.h
, déclarez la classe Worker
.path
, le constructeur et le signal newItem
.ImageLibrary.cpp
, codez le constructeur Worker (const QString & path)
.process
, sa file d'attente et sa boucle de traitement.ImageLibrary.cpp
et déclarez-la dans ImageLibrary.h
:
void ImageLibrary::addItem (const QString & item)
{
QStringList list = model.stringList ();
model.setStringList (list << item);
}
ImageLibrary::go
, créez une instance worker
avec le chemin sélectionné.newItem
de worker
à la méthode addItem
de this
.worker
en appelant sa méthode process
.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 ();
Worker
de telle sorte que l'on puisse la rattacher
à un QThread.ImageLibrary::go
pour rendre la tâche principale asynchrone.En complément de l'approche bas niveau, Qt propose le framework QtConcurrent.
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 &)
.
ImageLibrary.h
, déclarez la classe Item
, ses attributs et son constructeur.ImageLibrary.cpp
, implémentez le constructeur.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.
ImageLibrary.h
, déclarez la classe Model
, son attribut et son constructeur par défaut.ImageLibrary.cpp
, implémentez le constructeur.Model::rowCount
.Model::data
en vous référant à Qt::ItemDataRole.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.
Model::addItem
et implémentez-la.ImageLibrary
par Model
.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.
Worker::newItem
pour qu'elle coïncide avec celle de Model::addItem
.Worker::Thumbnail
et implémentez-la.Worker::process
pour générer des miniatures.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 :
Nous préférerons évidemment la première solution, plus simple et plus élégante.
Worker::MappedItem
qui générera un item.Worker::process
pour générer les miniatures en parallèle.Worker::processItem
qui émettra le signal Worker::newItem
.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.
Worker::newItem
.Model::addItem
en conséquence.Worker::newThumbnail
.Model::setThumbnail
qui définira la miniature d'un item.Worker::process
pour générer les miniatures de façon asynchrone.