La librairie Qt - 8
La liste hiérarchisée QListView
Le mois dernier, nous avons découvert la liste simple QListBox. Je vous présente ce mois-ci un widget de liste plus complexe, offrant plusieurs colonnes et une hiérarchisation des entrées, à la façon d'un gestionnaire de fichiers. Le programme du mois sera un prétexte pour découvrir d'autres petites choses de Qt...
Vous connaissez sans doute le petit
utilitaire du
, qui donne la taille occupée par un répertoire et
ses sous-répertoires. Nous allons en créer une version graphique (simplifiée,
sans options), qui ressemblera à ceci :
L'utilisateur aura la possibilité, soit de saisir un chemin (absolu) dans
le champ de saisi puis de valider par Entrée, soit de cliquer sur le bouton «
... » sur la droite pour obtenir une fenêtre de sélection de répertoire. Dans
les deux cas, le chemin sera parcouru et la liste centrale remplie. Pour
simplifier, aucun contrôle n'est prévu de la validité du répertoire donné, et
aucune des options de du ne sera implémentée. Donc évitez de tester ce
programme à partir de la racine d'un système sur lequel des systèmes de
fichiers réseau ou CD-ROM sont montés, avec pleins de liens symboliques dans
tous les sens, il risque de tourner longtemps avant de vous rendre la main...
Par ailleurs les tailles sont données en octets. Comme exercice, essayez de
coder les options comme -h
ou -x
!
Comme le mois précédent, nous allons inscrire les différentes déclarations
dans un fichier d'en-tête, qdu.h
ce mois-ci, et les
implémentations dans un autre fichier, qdu.cpp
. Comme nous
allons définir nos propres slots, nous devons passer par
moc
:
$ moc -o moc_qdu.cpp qdu.h
Puis la compilation se fera par :
$ g++ -o qdu.x -I$QTDIR/include -L$QTDIR/lib -lqt moc_qdu.cpp qdu.cpp
Rien de nouveau dans tout cela.
De loin, la classe QListView
qui nous intéresse aujourd'hui ressemble fort à la classe
QListBox
vue le mois dernier : elle permet d'afficher une
liste d'élements, et dérive de QScrollView
, ce qui automatise la
gestion des barres de défilement. Elle propose différents modes de sélection.
Chaque élément de la liste est un item, représenté par
QListViewItem
.
C'est la première différence notable.
Tandis qu'une instance de QListBoxItem
ne peut avoir qu'une
instance de QListBox
pour widget parent, un
QListViewItem
accepte pour parent, soit une
QListView
, soit un autre QListViewItem
: c'est de
cette façon que la hiérarchie est représentée. Par exemple, dans la capture
d'écran précédente, l'item advancemame a pour parent l'item
app-emulation, qui a lui-même pour parent l'item
/usr/portage, qui a - enfin - pour parent la
QListView
elle-même.
Autre différence importante, la
possibilité de définir plusieurs colonnes. La méthode
addColumn()
permet d'ajouter une colonne à la fin, en passant
son titre en paramètre. removeColumn()
permet de supprimer une
colonne repérée par son indice (la colonne la plus à gauche ayant l'indice
0), setColumnText()
et columnText()
permettent
respectivement de changer et d'obtenir le titre d'une colonne donnée. Par
ailleurs, les lignes de la liste peuvent être ordonnées selon n'importe
quelle colonne. Le tri s'effectue par une simple comparaison des chaînes de
caractères dans la colonne donnée, mais il est possible de définir son propre
tri en surdéfinissant la méthode compare()
de
QListViewItem
.
Il est parfois nécessaire de parcourir les items d'une liste, par exemple pour savoir lesquels sont sélectionnés - cette information n'étant pas directement disponible. Pour une liste ne contenant qu'un seul niveau, cette boucle peut faire l'affaire :
1: QListView * qlv = /* ... */ ; 2: QListViewItem * lvi = qlv->firstChild() ; 3: while ( lvi ) 4: { /* utiliser l'item */ 5: lvi = lvi->nextSibling() ; 6: }
La méthode firstChild()
de
QListView
nous donne un pointeur vers son premier item. On peut
obtenir le dernier avec lastItem()
, ou celui ayant actuellement
le focus avec currentItem()
. Un pointeur nul est retourné si la
liste est vide. Ensuite, on obtient un pointeur vers l'item suivant avec la
méthode nextSibling()
de QListViewItem
, qui
retourne un pointeur nul s'il n'y a pas de suivant. Attention,
nextSibling()
ne renvoit l'item suivant que dans le même niveau
!
Dans le cas d'une liste à plusieurs niveau, c'est à peine plus compliqué. La nature récursive de la hiérarchisation incite tout naturellement à définir une fonction récursive pour le parcours :
01: void TraiterItem(QListViewItem * lvi) 02: { /* utiliser l'item */ 03: // parcours les enfants 04: QListViewItem * enfant = lvi->firstChild() ; 05: while ( enfant ) 06: { TraiterItem(enfant) ; 07: enfant = enfant->nextSibling() ; 08: } 09: } 10: /* ... */ 11: QListView * qlv = /* ... */ 12: QListViewItem * lvi = qlv->firstChild() ; 13: while ( lvi ) 14: { TraiterItem(lvi) ; 15: lvi = lvi->nextSibling() ; 16: }
La fonction récursive
TraiterItem()
assure le parcours des enfants d'un item donné.
Notez qu'il aurait été possible d'y intégrer le parcours des items frères
(sibling), mais ce n'est généralement pas souhaitable pour des
questions de souplesse d'utilisation.
Revenons à notre du
graphique. La librairie C standard propose un ensemble de fonctions
permettant de parcourir (récursivement) un répertoire, comme
opendir()
ou scandir()
(qui n'est toutefois pas
POSIX). En application de ses principes de simplicité et de portabilité, Qt
propose la classe QDir
pour manipuler un répertoire. Nous ne
nous intéresserons ici qu'à sa méthode entryInfoList()
, qui
retourne une liste d'instances de QFileInfo
, liste contenue dans
une instance de QFileInfoList
. La classe QFileInfo
permet d'obtenir diverse information sur un fichier, comme son nom
(fileName()
), son chemin absolu (absFilePath()
), ou
sa taille (size()
).
Par ailleurs, nous allons utiliser
également les facilités de la classe de chaîne de caractères de Qt, la classe
QString
. Celle-ci propose une représentation des chaînes
extrèmement puissante, facilitant grandement la réalisation d'opérations plus
ou moins complexes, comme le découpage ou la concaténation. On peut la
comparer à la classe string
de la STL du C++ (ou plutôt, à
wstring
, car QString
travaille en Unicode), bien je
trouve QString
plus simple à manipuler.
Toutes ces classes seront détaillées plus tard, pour l'heure elles vont simplement nous simplifier grandement la vie.
Comme le mois dernier, pour atteindre
notre objectif nous allons devoir définir nos items personnalisés -
c'est-à-dire, dériver QListViewItem
. Nous voulons que chaque
item parcours un répertoire (ce sera donc un paramètre au constructeur), et
nous donne en retour la taille totale de ce qu'il contient, inclus les
sous-répertoires. Je vous propose donc cette déclaration, qui est aussi le
début du fichier qdu.h
:
01: #ifndef __QDU_H__ 02: #define __QDU_H__ 1 03: #include <qlistview.h> 04: #include <qlineedit.h> 05: #include <qpushbutton.h> 06: #include <qframe.h> 07: #include <qstring.h> 08: class clDirItem : public QListViewItem 09: { public: 10: clDirItem(QListView* parent, const QString& path, const QString& abs_path) ; 11: clDirItem(QListViewItem* parent, const QString& path, const QString& abs_path) ; 12: unsigned long sizeDir() const { return mSize ; } 13: private: 14: void parseDir(const QString& path, const QString& abs_path) ; 15: unsigned long mSize ; 16: } ;
QListViewItem
est déclarée dans
<qlistview.h>
.QListViewItem
: le premier
est utilisé lorsque notre item est intégré dans une liste au plus haut
niveau, le deuxième lorsque qu'il est enfant d'un autre item, la
distinction se faisant sur le premier paramètre. Les deux autres
paramètres sont, respectivement, le nom qui sera affiché dans la liste,
et le chemin absolu du répertoire à parcourir. parseDir()
sera la méthode qui va calculer la
taille, éventuellement en descendant les sous-répertoires. Voyons maintenant l'implémentation. Celle
des constructeurs est assez triviale. La voici, avec le début du fichier
qdu.cpp
:
#include <qdir.h> #include <qfileinfo.h> #include <qlayout.h> #include <qapplication.h> #include <qfiledialog.h> #include <qlabel.h> #include "qdu.h" clDirItem::clDirItem(QListView* parent, const QString & path, const QString& abs_path) : QListViewItem(parent), mSize(0) { parseDir(path, abs_path) ; } clDirItem::clDirItem(QListViewItem* parent, const QString & path, const QString& abs_path) : QListViewItem(parent), mSize(0) { parseDir(path, abs_path) ; }
Dans cette implémentation, le parcours du
répertoire est lancé dès la création de l'instance de
QListViewItem
. Rien de bien compliqué, passons à la méthode
intéressante :
01: void clDirItem::parseDir(const QString& path, const QString& abs_path) 02: { QDir dir(abs_path) ; 03: QFileInfoList* infos = const_cast<QFileInfoList*>(dir.entryInfoList()) ; 04: QFileInfo* iter = 0 ; 05: for (iter = infos->first() ; iter ; iter = infos->next()) 06: { if ( iter->isDir() && 07: (iter->fileName() != ".") && 08: (iter->fileName() != "..") ) 09: { clDirItem * dir_item = 10: new clDirItem(this, iter->fileName(), iter->absFilePath()) ; 11: mSize += dir_item->sizeDir() ; 12: } 13: else 14: mSize += iter->size() ; 15: } 16: setText(0, path) ; 17: setText(1, QString::number(mSize)) ; 18: }
QDir
sur le répertoire à parcourir.QFileInfoList
. L'utilisation
de const_cast<>
est nécessaire, car le pointeur
renvoyé par QDir::entryInfoList()
est qualifié constant, or
pour parcourir la liste nous allons devoir utiliser les méthodes
first()
et next()
de QFileInfoList
(ligne 5), qui ne sont pas qualifiées constantes... Le procédé est un peu
cavalier, normalement nous devrions travailler sur une copie de la liste,
avec quelques chose comme QFileInfoList
ma_liste(*(dir.entryInfoList()))
. Toutefois nos besoins précis ne
justifient pas l'encombrement mémoire associé. Notez bien que la liste
appartient à l'instance de QFileInfo
, aussi n'essayez
surtout pas de la modifier, encore moins de la détruire !for
. Pour ce type de liste le
parcours est assez typique d'une liste chaînée. Les parcours des
différentes structures de données made in Qt seront abordés plus
tard.isDir()
de QFileInfo
renvoit vrai si l'élément est un répertoire. Dans ce cas, nous devons
descendre d'un niveau, mais pour éviter une récursion infinie, nous ne
traitons pas le cas où il s'agit des répertoires spéciaux
".
" et "..
". L'opérateur !=
peut
vous sembler étrange ici, mais il fonctionne comme attendu, car
fileName()
renvoit une instance de QString
, et
les chaînes en style C sont alors automatiquement transtypées en
QString
- pour laquelle l'opérateur !=
, entre
autres, est défini.absFilePath()
de
QFileInfo
donne le chemin complet et absolu d'un
fichier.setText()
de
QListViewItem
permet de définir le texte (deuxième
paramètre) devant apparaître dans chaque colonne (premier paramètre). La
méthode statique number()
de QString
permet de
convertir facilement un nombre en chaîne (en fait, en
QString
).Voilà implémentés nos items, adaptés à notre besoin. Comme pour les
QListBoxItem
, on peut personnaliser davantages ce qu'affichent
nos items, en surdéfinissant la méthode paintCell()
. Consultez
la documentation de Qt pour plus de détails !
Il est temps de passer à la création de notre fenêtre principale. Je vous
propose la déclaration suivante, qui termine le fichier qdu.h
:
01: class wMain : public QWidget 02: { Q_OBJECT 03: public: 04: wMain() ; 05: public slots: 06: void slotBtBrowse() ; 07: void slotPathChanged() ; 08: private: 09: QLineEdit * lePath ; 10: QPushButton * btBrowse ; 11: QPushButton * btQuit ; 12: QListView * lvDirTree ; 13: } ; 14: #endif // __QDU_H__
Notez la déclaration de deux slots, le
premier appellé lorsque l'utilisateur clique sur le bouton "...
"
en haut à droite, le second lorsque le texte de la zone de saisie est
modifié.
Voici quel pourrait être l'implémentation
du constructeur de cette classe (dans qdu.cpp
) :
01: wMain::wMain() 02: { QVBoxLayout * vbox = new QVBoxLayout(this) ; 03: QHBoxLayout * hbox = new QHBoxLayout(vbox) ; 04: hbox->addWidget(new QLabel("Répertoire : ", this)) ; 05: lePath = new QLineEdit(this) ; 06: hbox->addWidget(lePath) ; 07: btBrowse = new QPushButton("...", this) ; 08: hbox->addWidget(btBrowse) ; 09: lvDirTree = new QListView(this) ; 10: lvDirTree->setAllColumnsShowFocus(true) ; 11: lvDirTree->setRootIsDecorated(true) ; 12: lvDirTree->addColumn("Chemin") ; 13: lvDirTree->addColumn("Taille") ; 14: lvDirTree->setColumnAlignment(1, Qt::AlignRight) ; 15: vbox->addWidget(lvDirTree) ; 16: btQuit = new QPushButton("Quit", this) ; 17: vbox->addWidget(btQuit) ; 18: connect (lePath, SIGNAL(returnPressed()), 19: this, SLOT(slotPathChanged())) ; 20: connect (btBrowse, SIGNAL(clicked()), 21: this, SLOT(slotBtBrowse())) ; 22: connect (btQuit, SIGNAL(clicked()), 23: qApp, SLOT(quit())) ; 24: }
lignes 1 à 8 : construction de la ligne du haut, les manipulations sur les layouts vous sont maintenant familière, n'est-ce pas ?
ligne 9 :
création de notre QListView
.
ligne 10 :
cette méthode, setAllColumnsShowFocus()
, indique que l'on
veut que toutes les colonnes affichent le focus (en général, texte blanc
sur fond bleu sombre) lorsqu'une ligne est sélectionné. Sans cela, seule
la première colonne afficherait la marque de focus.
ligne 11 : par
défaut, les QListView
sont « plates », n'offrent
pas de hiérarchie. L'appel à cette méthode avec true
comme
paramètre impose de dessiner les petits symboles permettant de dérouler
une sous-liste, lorsqu'un item possède des items enfants.
lignes 12 et 13 : ajout des colonnes, avec leur titres.
ligne 14 : simplement pour faire joli, on demande à ce que la deuxième colonne, celle des tailles, soit alignée à droite : c'est l'alignement usuel pour représenter des nombres en colonnes.
lignes 15 à 24 : la fin du constructeur, sans surprise, avec les diverses connexions. Remarquez tout de même la connexion ligne 18 : une modification du champ de saisi n'est prise en compte que lorsque que l'utilisateur valide avec Entrée.
Voyons ce qui se passe lorsque
l'utilisateur clique sur le bouton "...
" :
void wMain::slotBtBrowse() { QString path = QFileDialog::getExistingDirectory() ; if ( ! (path.isEmpty() || path.isNull()) ) { lePath->setText(path) ; slotPathChanged() ; } }
Je fais ici appel à une méthode statique
de la classe QFileDialog
(déclarée dans
<qfiledialog.h>
), qui ouvre une fenêtre permettant de
sélectionner un répertoire existant. Cette classe propose d'autres méthodes
statiques, pour sélectionner un fichier existant
(getOpenFileName()
), un fichier par forcément existant
(getSaveFileName()
, par exemple pour une sauvegarde), ou
plusieurs fichiers existants (getOpenFileNames()
). Ces méthodes
permettent de donner un répertoire de départ ainsi que des filtres pour
spécifier le type de fichier voulu. Elles renvoient une chaîne (ou une liste
de chaînes), vide si l'utilisateur a annulé la boîte de dialogue. Ici, nous
fixons le texte de la zone de sasie, et appelons le slot associé.
Remarquez sur la capture d'écran que la boîte de dialogue offre les icônes nécessaires pour changer le type d'affichage (avec ou sans détails), ainsi que pour créer un répertoire.
Voici enfin la méthode qui déclenche le calcul de la taille du répertoire sélectionné :
1: void wMain::slotPathChanged() 2: { lvDirTree->clear() ; 3: lvDirTree->setColumnWidth(0, 50) ; 4: lvDirTree->setColumnWidth(1, 50) ; 5: lvDirTree->setOpen( 6: new clDirItem(lvDirTree, lePath->text(), lePath->text()), true) ; 7: }
ligne 2 : la
méthode clear()
de QListView
permet de vider la
liste, c'est-à-dire de retirer tous ses items. Attention, les items ainsi
retirés sont automatiquement détruits (avec delete
). Si vous
voulez retirer un item particuliers, utilisez takeItem()
.
lignes 3 et 4
: par défaut, la liste détermine elle-même la largeur de ses colonnes.
Toutefois, l'appel à clear()
ne réinitialise pas la largeur
courante : on risque donc d'avoir des colonnes trop larges pour le
nouveau contenu. Ces deux lignes forcent un redimensionnement des
colonnes à une petite taille, afin qu'elles puissent par la suite grandir
pour s'adapter au nouveau contenu.
ligne 5 : nous
créons un nouvel item (new clDirItem(...)
), qui va être
inséré dans notre liste, grâce au premier paramètre du constructeur. Le
simple fait de créer l'item va lancer le parcours récursif des
répertoires, comme nous l'avons vu plus haut. Enfin la méthode
setOpen()
demande à ce que cet item soit
« déroulé », c'est-à-dire que son éventuel contenu soit
visible, comme si on avait cliqué sur le "+" à sa gauche.
Il ne nous reste plus qu'à écrire une
fonction main()
, très classique :
int main(int argc, char* argv[]) { QApplication app(argc, argv) ; wMain main ; app.setMainWidget(&main) ; main.show() ; return app.exec() ; }
Nous voici avec une version graphique et
portable de du
!
Si vous utiliser le programme sur un
répertoire contenant beaucoup de fichiers et/ou sous-répertoires (essayez
/usr/share/doc
par exemple), vous remarquerez que le programme
semble figé pendant le parcours des répertoires. La fenêtre ne se rafraîchit
même pas si vous la masquez.
Dans ce genre de situations, il est
préférable d'afficher un petit quelque chose qui montre à l'utilisateur que
le programme est encore en vie - par exemple, une barre de progression. Qt
nous fournit justement une barre de progression prête à l'emploi. Dans les
#include
de qdu.cpp
, ajoutez la ligne :
#include <qprogressdialog.h>
et modifiez la méthode
clDirItem::parseDir()
ainsi :
01: /* ... */ 02: QFileInfo* iter = 0 ; 03: QProgressDialog progress(QString("Processing %1...").arg(abs_path), 04: "Stop", infos->count()) ; 05: unsigned cpt = 0 ; 06: for (iter = infos->first() ; iter ; iter = infos->next()) 07: { if ( infos->count() > 100 ) 08: { progress.setProgress(cpt++) ; 09: qApp->processEvents() ; 10: } 11: if ( iter->isDir() /* ...etc...*/ 12:
QProgressDialog
, qui
va afficher le répertoire en cours de traitement. De plus un compteur est
déclaré, pour savoir où l'on en est.setProgress()
attent en argument
l'étape où l'on est). L'appel à la méthode processEvents()
de QApplication
(ligne 9) est nécessaire : sans elle, les
messages de rafraîchissement de la fenêtre seraient stockés par le
système graphique, et auraient peu de chance d'être exécutés, étant donné
que notre parcours de laisse aucune pause pour redessiner
l'affichage.Pour mémoire, qApp
(déclaré
dans <qapplication.h>
) est un pointeur vers l'instance de
QApplication
de l'application.
À vous de jouer maintenant, pour fournir une interface plus sophistiquée (comme affichant les tailles en Ko plutôt qu'en octets) ou acceptant les paramètres en ligne de commande !
Les quelques widgets que nous avons étudiés depuis le commencement de cette série d'articles vous permettent d'ores et déjà de construire des applications assez complexes. Beaucoup restent encore inexplorés. Avant de poursuivre notre chemin dans les widgets de Qt, nous allons nous intéresser le mois prochain à quelques-uns des aspects non graphiques de Qt, comme sa classe de chaîne de caractères, ses structures de données ou ses services systèmes.
Yves Bailly
Article publié dans LinuxMagazine 44 de novembre 2002