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...

  1. Organisation
  2. Le widget QListView et ses items
  3. Parcourir les items de la liste
  4. Aperçu des classes QDir et QFileInfo
  5. Nos items personnalisés
  6. La fenêtre principale
  7. Juste une fioriture...
  8. Conclusion

codes sources

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 :

qdu

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 !

Organisation

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.

Le widget QListView et ses items

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.

Parcourir les items de la liste

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.

Aperçu des classes QDir et QFileInfo

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.

Nos items personnalisés

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: } ;

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: }

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 !

La fenêtre principale

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: }

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é.

getExistingDirectory

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: }

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 !

Juste une fioriture...

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:  

Pour mémoire, qApp (déclaré dans <qapplication.h>) est un pointeur vers l'instance de QApplication de l'application.

QProgressDialog

À 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 !

Conclusion

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

http://www.kafka-fr.net


Article publié dans LinuxMagazine 44 de novembre 2002