Traduction par Jean-luc Biord, du Site de la communauté Qt francophone.
English TOC.

13. Listes, Arbres et Tableaux

La plupart des applications tendent à afficher des données sous forme de listes, de tables et d'arbres. Ce besoin est satisfait par Qt avec un certain nombre de classes s'étendant des listes-textes simples aux tables complexes. Nous commençons simplement par traiter QListBox représenté sur le schéma 13-1.

QListBox

The QListBox example application

Schéma13-1 L'application d'exemple QListBox

QListBox est la forme la plus simple de liste. Il est normalement employé pour montrer une colonne simple d'éléments de texte mais peut être utilisé dans des contextes plus avancés. En montrant une liste d'éléments de texte les méthodes insertItem ou insertStringList sont employés pour construire la liste. Pour nettoyer la liste, employez la méthode clear pour enlever tous les articles de la liste. C'est ce qui est montré dans le code dans l'exemple 13-1.

ListBoxExample::ListBoxExample( QWidget *parent, char *name ) : QVBox( parent, name )
{
  m_listBox = new QListBox( this );
  
  QHBox *hb = new QHBox( this );
  m_lineEdit = new QLineEdit( hb );
  QPushButton *pbAdd = new QPushButton( "Add", hb );
  QPushButton *pbClear = new QPushButton( "Clear", hb );

  connect( pbAdd, SIGNAL(clicked()), this, SLOT(addItem()) );
  connect( pbClear, SIGNAL(clicked()), m_listBox, SLOT(clear()) );
}

void ListBoxExample::addItem()
{
  m_listBox->insertItem( m_lineEdit->text() );
  m_lineEdit->setText( "" );
}

Exemple 13-1

Le code d'exemple montre l'implémentation de démonstration du widget QListBox. Les pointeurs m_listBox et m_lineEdit sont des membres privés de la classe. Notez comment clear peut être relié directement à un signal.

Maintenant nous savons remplir et nettoyer la liste. Comment savoir quels articles sont sélectionnés et comment réagir à cela ? Un widget QListBox peut avoir trois modes différents de sélection : simple, multiple et étendu.

Un quatrième mode est également disponible qui empêche simplement l'utilisateur de choisir quoi que ce soit. Le mode de choix par défaut est simple. Maintenant, comment savoir lorsqu'un élément est choisi, et quel est cet élément ? Pour tester cela, nous ajoutons un label en dessous de notre widget et un slot qui le modifie avec le choix courant. Dans le constructeur nous relions le signal selectionChanged à notre slot. Le code est montré dans l'exemple 13-2.

ListBoxExample::ListBoxExample( QWidget *parent, char *name ) : QVBox( parent, name )
{
  ...
  connect( m_listBox, SIGNAL(selectionChanged()), this, SLOT(newSelection()) );
}

void ListBoxExample::newSelection()
{
  if( !m_listBox->selectedItem() )
    m_label->setText( "nothing" );
  else
    m_label->setText( m_listBox->selectedItem()->text() );
}

Exemple 13-2

Essayez le code d'exemple en jouant avec les boutons pour généré des signaux. Par exemple, notez que selectionChanged est mieux employé avec une liste à contenu statique puisque le signal n'est pas émis lorsque la liste est nettoyée.

Il est possible de sous-classer la classe QListBoxItem afin de fournir des listes plus avancées. Des exemples sont QListBoxPixmap et QjListBoxPixmap qui fournissent à un élément de boîte de liste un texte et une image. Le code dans l'exemple 13-3 montre comment installer une boîte de liste avec ce type d'éléments au lieu des éléments seulement textes.

QListBox *lb = new QListBox();
QPixmap pm( 12, 12 );

pm.fill( Qt::red );
new QListBoxPixmap( lb, pm, "Red" );
pm.fill( Qt::yellow );
new QListBoxPixmap( lb, pm, "Yellow" );
pm.fill( Qt::green );
new QListBoxPixmap( lb, pm, "Green" );
pm.fill( Qt::cyan );
new QListBoxPixmap( lb, pm, "Cyan" );
pm.fill( Qt::blue );
new QListBoxPixmap( lb, pm, "Blue" );
pm.fill( Qt::magenta );
new QListBoxPixmap( lb, pm, "Magenta" );

Exemple 13-3

L'application résultante est montrée sur le schéma 13-2. Dans le code, notez que les éléments de boîte de liste sont instanciés avec une référence de la boîte de liste. Ceci les fait s'ajouter à la liste automatiquement. Comparez ceci à la façon dont des articles texte seulement sont ajoutés à la liste. Il est également possible d'employer la méthode insertItem avec les éléments personnalisés. Dans ce cas, le code ressemblerait au slot addItem de l'exemple 13-1.

A QListBox with QListBoxPixmaps

Schéma13-2 Une QListBox avec des QListBoxPixmaps

QListView

En traitant des données plus complexes, multicolonnes, le widget QListView convient mieux que QListBox.QListView peut afficher des données multicolonnes stockées comme liste ou dans une structure arborescente. Chaque colonne peut avoir un en-tête qui permet de trier en cliquant sur les titres. Le schéma 13-3 montre une capture d'écran de l'application donnée en exemple.

The list tab

Schéma 13-3 La liste à onglet

Comme le widget listview est assez complexe, l'exemple est coupé en quatre parties, chacune montrée dans un onglet différent dans l'application d'exemple. Nous commençons par le cas le plus fondamental - une liste simple avec les colonnes multiples. C'est montré dans l'onglet intitulé "List" représenté sur le schéma 13-3. Le code installant la listview est montré dans l'exemple 13-4.

QWidget *ListViewExample::setupListTab()
{
  m_listView = new QListView();
  
  m_listView->addColumn( "Foo" );
  m_listView->addColumn( "Bar" );
  m_listView->addColumn( "Baz" );
  
  m_listView->setAllColumnsShowFocus( true );
  
  new QListViewItem( m_listView, "(1, 1)", "(1, 2)", "(1, 3)" );
  new QListViewItem( m_listView, "(2, 1)", "(2, 2)", "(2, 3)" );
  new QListViewItem( m_listView, "(3, 1)", "(3, 2)", "(3, 3)" );
  new QListViewItem( m_listView, "(4, 1)", "(4, 2)", "(4, 3)" );
  
  return m_listView;
}

Exemple 13-4

Le code montre les étapes de base pour utiliser une vue de liste. D'abord, un certain nombre de colonnes est créé. Puis, quelques arrangements spéciaux sont effectués. Dans ce cas-ci, nous nous assurons que le choix est affiché sur la ligne entière, et pas simplement sur la première colonne. Finalement, nous remplissons la liste en créant un ensemble de QListViewItems se référant à la vue de liste comme parent.

The tree tab

Schéma 13-4 L'arbre à onglets

Le deuxième cas d'utilisation est de montrer les éléments de liste dans un arbre hiérarchique. C'est montré dans l'onglet intitulé "Tree". Le code installant la liste est montré dans l'exemple 13-5. En exécutant l'application d'exemple, essayez de cliquer sur les en-têtes de colonne pour trier les éléments. Notez que chaque branche ("A", "B" et "C") est triée séparément.

QWidget *ListViewExample::setupTreeTab()
{
  m_treeView = new QListView();
  
  m_treeView->addColumn( "Tree" );
  m_treeView->addColumn( "First" );
  m_treeView->addColumn( "Second" );
  m_treeView->addColumn( "Third" );
  
  m_treeView->setRootIsDecorated( true );

  QListViewItem *root = new QListViewItem( m_treeView, "root" );
  
  QListViewItem *a = new QListViewItem( root, "A" );
  QListViewItem *b = new QListViewItem( root, "B" );
  QListViewItem *c = new QListViewItem( root, "C" );
  
  new QListViewItem( a, "foo", "1", "2", "3" );
  new QListViewItem( a, "bar", "i", "ii", "iii" );
  new QListViewItem( a, "baz", "a", "b", "c" );

  new QListViewItem( b, "foo", "1", "2", "3" );
  new QListViewItem( b, "bar", "i", "ii", "iii" );
  new QListViewItem( b, "baz", "a", "b", "c" );

  new QListViewItem( c, "foo", "1", "2", "3" );
  new QListViewItem( c, "bar", "i", "ii", "iii" );
  new QListViewItem( c, "baz", "a", "b", "c" );
  
  return m_treeView;
}

Exemple 13-5

Le code est structuré comme la liste de base. La différence est qu'au lieu d'employer le widget liste comme parent pour tous les articles, quelques articles ont d'autres articles comme parents. La raison de placer la décoration de l'article root est de fournir de l'information à l'utilisateur. Voir juste l'article "root" n'indique pas à l'utilisateur qu'il y a plus d'articles. Le plus à la droite du texte indique cela à l'utilisateur.

Sorting per column

Schéma13-5 Tri par colonne

Le troisième cas d'utilisation démontre un problème commun que la plupart des utilisateurs de fenêtres rencontrent. Si vous essayez de trier des nombres tout en les traitant comme chaîne de caractères, le numéro dix finira positionné entre le numéro un et le numéro deux. La plus facile, mais pas la plus jolie solution est de compléter les nombres avec les zéros absents à la gauche du nombre ou de traiter les chaînes comme valeurs au lieu des morceaux de texte.

class SortItem : public QListViewItem
{
public:
  SortItem( QListView *parent, QString c1, QString c2, QString c3, QString c4 ) : QListViewItem( parent, c1, c2, c3, c4 )
  {
  }
  
  int compare( QListViewItem *i, int col, bool asc ) const
  {
    if( col == 2 )
      return text( col ).toInt() - i->text( col ).toInt();
    else
      return QListViewItem::compare( i, col, asc );
  }
};

Exemple 13-6

L'exemple de tri emploie une classe personnalisée de liste montrée dans l'exemple 13-6. Les éléments de classe personnalisée réimplémentent la méthode compare et traite la troisième colonne comme un nombre au lieu d'une chaîne de caractères. Les autres colonnes emploient la méthode standard QListViewItem::compare.

Essayez d'exécuter l'exemple comme représenté sur le schéma 13-5. Triez sur les différentes colonnes et notez comment cela fonctionne. En outre, notez comment le tri de la quatrième colonne, en texte seulement, tient compte du cas des caractères. Le code installant la liste triée est montré dans l'exemple 13-7.

QWidget *ListViewExample::setupSortTab()
{
  m_sortView = new QListView();
  
  m_sortView->addColumn( "Number" );
  m_sortView->addColumn( "Padded" );
  m_sortView->addColumn( "Corrected" );
  m_sortView->addColumn( "Alphabetical" );
  
  m_sortView->setShowSortIndicator( true );
  
  new SortItem( m_sortView, "1", "02", "1", "foo" );
  new SortItem( m_sortView, "3", "04", "1", "bar" );
  new SortItem( m_sortView, "5", "06", "2", "foo" );
  new SortItem( m_sortView, "7", "08", "3", "bar" );
  new SortItem( m_sortView, "9", "10", "5", "Foo" );
  new SortItem( m_sortView, "11", "12", "8", "bar" );
  new SortItem( m_sortView, "13", "14", "13", "foo" );
  new SortItem( m_sortView, "15", "00", "21", "Bar" );
  
  return m_sortView;
}

Exemple 13-7

Icons in a QListView

Figure 13-6 Icons in a QListView

L'onglet "Icons" montre à quel point il est facile de mettre des icônes dans une liste. Il peut y avoir différents éléments dans différentes colonnes et de multiples icônes sur chaque ligne. Le code source est montré dans l'exemple 13-8. Comme vous pouvez voir, mettre une icône en colonne est aussi facile qu'appeler setPixmap.

QWidget *ListViewExample::setupIconTab()
{
  m_iconView = new QListView();

  m_iconView->addColumn( "Foo" );
  m_iconView->addColumn( "Bar" );
  m_iconView->addColumn( "Baz" );
  
  m_iconView->setAllColumnsShowFocus( true );
  
  QPixmap pm( 10, 10 );
  
  QListViewItem *lvi = new QListViewItem( m_iconView, "(1, 1)", "(1, 2)", "(1, 3)" );
  pm.fill( Qt::red );
  lvi->setPixmap( 0, pm );
  
  lvi = new QListViewItem( m_iconView, "(2, 1)", "(2, 2)", "(2, 3)" );
  pm.fill( Qt::green );
  lvi->setPixmap( 1, pm );

  lvi = new QListViewItem( m_iconView, "(3, 1)", "(3, 2)", "(3, 3)" );
  pm.fill( Qt::blue );
  lvi->setPixmap( 2, pm );

  lvi = new QListViewItem( m_iconView, "(4, 1)", "(4, 2)", "(4, 3)" );  
  pm.fill( Qt::yellow );
  lvi->setPixmap( 0, pm );
  pm.fill( Qt::magenta );
  lvi->setPixmap( 2, pm );
  
  return m_iconView;
}

Exemple 13-8

Travailler avec listview est semblable à travailler avec listbox. Le signal selectionChanged est encore disponible avec les méthodes isSelected et setSelected. Dans des applications modernes, je préfère réellement la listview à la listbox parce que la listview fournit des en-têtes de colonne qui fournissent plus d'informations aux utilisateurs.

Un problème quand on travaille avec n'importe quelle liste est de coordonner le contenu de la liste et le contenu des données d'application. Par exemple, disons que vous avez une application d'annuaire téléphonique. Chaque élément d'annuaire téléphonique a une image et une partie détaillée qui lui est associée comme le nom, le numéro de téléphone, l'email, l'adresse, etc. La listview montrant une liste de contact n'est pas prévue pour montrer toute l'information. Peut-être juste les noms des contacts sont intéressants jusqu'à ce qu'un article soit choisi. Il y a plusieurs approches pour traiter ce problème :

  1. Les contacts sont stockés dans une base de données. Chaque contact a un identifiant unique pour les séparer (pour permettre les contacts multiples avec le même nom). Cette identifiant n'est jamais montré à l'utilisateur et est toujours caché. En mettant cette identifiant dans une colonne cachée et en l'employant pour rechercher le bon contact dans un QValueList ou un QMap la coordination peut-être faite.
  2. Les contacts sont lus dans la mémoire et stockés dans une liste. Un élément spécial de listview prenant un contact comme entrée et réimplémentant la méthode text extrait l'information demandée. En changeant les données, seulement le contact est changé. L'élément de listview seulement doit être redessiné pour refléter le changement.
  3. La classe QListViewItem est sous-classée et la méthode text est réimplémentée. La nouvelle classe, ContactItem, contient toute l'information requise et peut toujours être employée dans une listview. Le problème surgit quand une deuxième vue du même élément est requise.
  4. La synchronisation de la liste de contacts et des éléments de la liste peut être incluse dans les actions d'édition. Chaque action doit faire attention à changer l'élément de listview et l'élément de contact de la même manière. Ceci peut facilement se développer hors de toute réflexion - et - polluer le code des actions.

Les trois premières solutions sont celles que je préfère. La troisième uniquement si une vue est exigée, la deuxième si toutes les données sont contenues dans la mémoire et la première si les données sont trop grandes pour tenir en mémoire. Pour mettre en application la première solution, une colonne cachée est nécessaire. Selon cette astuce, toute colonne qui doit être cachée a besoin d'avoir le mode de largeur réglé sur manuel. Le code pour cacher la colonne trois de la vue de liste, lv, y compris le changement du mode de largeur, est montré dans l'exemple 13-9.

lv->setColumnWidthMode( 3, QListView::Manual );
lv->hideColumn( 3 );

Exemple 13-9

QTable

Si les données sont plus structurées et complexes, les boîtes listview et listbox peuvent sembler trop simples. Dans ce cas, une table est nécessaire. En utilisant un widget QTable chaque cellule de table peut avoir un widget différent. Par exemple, des cases à cocher, les listes déroulantes et de simples textes peuvent être contenus dans une cellule.

Il est facile de créer une table et de la remplir avec des éléments en utilisant les méthodes setText, setPixmap et setItem.

Le secret, en utilisant des tables, est de pouvoir créer des éléments personnalisés. C'est employé pour fournir un meilleur confort aux utilisateurs. Par exemple, au lieu de choisir le nom d'une couleur à partir d'une liste déroulante, nous pouvons fournir un élément qui affiche une liste des couleurs réelles et laisse l'utilisateur sélectionner l'une d'entre elles. Une capture d'écran de l'application est montrée sur le schéma 13-7.

A QTable with ColorTableItems

Schéma 13-7 Une QTable avec des ColorTableItems

Ce que nous avons fait est de sous-classer QTableItem et de créer la nouvelle classe : ColorTableItem. La déclaration de classe est montrée dans l'exemple 13-10.

class ColorTableItem : public QTableItem
{
public:
  ColorTableItem( QTable *table, const QString &color );
  
  QWidget *createEditor() const;
  void setContentFromEditor( QWidget *w );
  
  void paint( QPainter *p, const QColorGroup &cg, const QRect &cr, bool selected );
};

Exemple 13-10

En regardant le code ci-dessus, on peut voir que les méthodes intéressantes sont paint, createEditor et setContentFromEditor. Les deux dernières travaillent ensemble tandis que la première peut être implémentée indépendamment des autres.

La méthode paint prend le texte de cellule qui est supposé être le nom d'une couleur. Elle remplit alors la cellule de cette couleur. L'exemple 13-11 montre l'exécution de la méthode paint.

void ColorTableItem::paint( QPainter *p, const QColorGroup &cg, const QRect &cr, bool selected )
{
  if( text() == "White" )
    p->setBrush( Qt::white );
  else if( text() == "Gray" )
    p->setBrush( Qt::gray );
  else if( text() == "Black" )
    p->setBrush( Qt::black );
  else if( text() == "Red" )
    p->setBrush( Qt::red );
  else if( text() == "Yellow" )
    p->setBrush( Qt::yellow );
  else if( text() == "Green" )
    p->setBrush( Qt::green );
  else if( text() == "Cyan" )
    p->setBrush( Qt::cyan );
  else if( text() == "Blue" )
    p->setBrush( Qt::blue );
  else if( text() == "Magenta" )
    p->setBrush( Qt::magenta );

  p->drawRect( table()->cellRect(row(), col()) );
}

Exemple 13-11

createEditor et setContentFromEditor fonctionnent ensemble. D'abord, createEditor crée le widget qui est utilisé pour éditer la cellule, puis le résultat est récupéré du widget éditeur en utilisant setContentFromEditor. Ainsi, dans l'exemple 13-12 le code pour createEditor est montré. Il crée une boîte combo, qui est une boîte de liste déroulante, avec des articles de couleur. Il s'assure également que le bon article est choisi parmi la liste.

QWidget *ColorTableItem::createEditor() const
{
  QComboBox *cb = new QComboBox( table()->viewport() );
  QObject::connect( cb, SIGNAL( activated( int ) ), table(), SLOT( doValueChanged() ) );
  
  QPixmap pm( 100, 20 );
  
  pm.fill( Qt::white );
  cb->insertItem( pm );
  pm.fill( Qt::gray );
  cb->insertItem( pm );
  pm.fill( Qt::black );
  cb->insertItem( pm );
  pm.fill( Qt::red );
  cb->insertItem( pm );
  pm.fill( Qt::yellow );
  cb->insertItem( pm );
  pm.fill( Qt::green );
  cb->insertItem( pm );
  pm.fill( Qt::cyan );
  cb->insertItem( pm );
  pm.fill( Qt::blue );
  cb->insertItem( pm );
  pm.fill( Qt::magenta  );
  cb->insertItem( pm );

  if( text() == "White" )
    cb->setCurrentItem( 0 );
  else if( text() == "Gray" )
    cb->setCurrentItem( 1 );
  else if( text() == "Black" )
    cb->setCurrentItem( 2 );
  else if( text() == "Red" )
    cb->setCurrentItem( 3 );
  else if( text() == "Yellow" )
    cb->setCurrentItem( 4 );
  else if( text() == "Green" )
    cb->setCurrentItem( 5 );
  else if( text() == "Cyan" )
    cb->setCurrentItem( 6 );
  else if( text() == "Blue" )
    cb->setCurrentItem( 7 );
  else if( text() == "Magenta" )
    cb->setCurrentItem( 8 );
  
  return cb;
}

Exemple 13-12

Puis, comme montré dans l'exemple 13-13, setContentFromEditor prend simplement le widget éditeur, vérifie de sorte que ce soit une boîte combo et met le résultat de nouveau dans la cellule.

void ColorTableItem::setContentFromEditor( QWidget *w )
{
  if( w->inherits( "QComboBox" ) )
  {
    switch( ((QComboBox*)w)->currentItem() )
    {
      case 0:
        setText( "White" );
        break;
      case 1:
        setText( "Gray" );
        break;
      case 2:
        setText( "Black" );
        break;
      case 3:
        setText( "Red" );
        break;
      case 4:
        setText( "Yellow" );
        break;
      case 5:
        setText( "Green" );
        break;
      case 6:
        setText( "Cyan" );
        break;
      case 7:
        setText( "Blue" );
        break;
      case 8:
        setText( "Magenta" );
        break;
    }
  }
  else
    QTableItem::setContentFromEditor( w );
}

Exemple 13-13

Le code installant la table avec l'élément personnalisé est montré dans l'exemple 13-14. Notez que les noms de couleur doivent être valides pour qu'il fonctionne, mais cela peut être amélioré en utilisant un type énuméré, ou simplement un QColor.

QTable t( 3, 3 );
  
t.setItem( 0, 0, new ColorTableItem( &t, "Red" ) );
t.setItem( 0, 1, new ColorTableItem( &t, "Green" ) );
t.setItem( 0, 2, new ColorTableItem( &t, "Black" ) );
t.setItem( 1, 0, new ColorTableItem( &t, "Yellow" ) );
t.setItem( 1, 1, new ColorTableItem( &t, "Magenta" ) );
t.setItem( 1, 2, new ColorTableItem( &t, "Blue" ) );
t.setItem( 2, 0, new ColorTableItem( &t, "Gray" ) );
t.setItem( 2, 1, new ColorTableItem( &t, "Cyan" ) );
t.setItem( 2, 2, new ColorTableItem( &t, "White" ) );

Exemple 13-14

Il y a quelques limitations avec la classe QTable qui a ouvert un marché pour des solutions tierces. L'une d'entre elles est QicsTable d'ICS qui est disponible avec une licence duale comme celle de Qt. La version open source est sous licence GPL. Elle offre plus de flexibilité dans toutes les parties de la classe s'étendant d'une architecture modèle-vue-contrôleur aux cellules joignables, les entêtes plus flexibles, les rangées et les colonnes verrouillables, la capacité d'imprimer la totalité ou des parties de la table, etc. Il y a un papier fourni par ICS intitulé au-delà de QTable qui parle les différences entre QTable et QicsTable.

Qt 4 - Interview

Dans Qt 4, les classes QListView et QTable seront partiellement fusionnées comme partie de Interview framework. L'architecture sera basée autour des vues et d'un modèle contenant les données. Cela réduit le besoin de stocker l'information dans plusieurs endroits dans la mémoire centrale (c.-à-d. comme élément de vue de liste et dans une structure de données spécifiques d'application). Plus d'information au sujet du Qt 4 est fournie par Trolltech.

Résumé

Le code de ce chapitre peut être téléchargé ici.

  • Employez QListBox pour les listes simples.
  • Employez QListView pour des listes plus complexes et des listes où les en-têtes de colonne doivent être bons.
  • Employez QTable pour l'information tabulaire.
  • N'hésitez pas à sous-classer QListBoxItem, QListViewItem et QTableItem pour créer des éléments personnalisés. C'est une approche facile pour créer les éléments personnalisés d'interface utilisateur.
  • Lecture Recommandée

    Qt Quaterly a une paire d'articles qui sont en rapport avec le sujet de ce chapitre: 1 et 2.

    This is a part of digitalfanatics.org.