Traduzione a cura di Fabrizio Angius [qtsolutions -A-T- gmx.net]
English TOC.

13. Liste, alberi e tabelle

Generalmente, la maggior parte delle applicazioni mostra dei dati sotto forma di liste, tabelle e alberi. Qt viene incontro a questa necessita' con una serie di classi, dalla semplice lista per testo alle tabelle piu' complesse. Iniziamo dalla parte piu' facile parlando della classe QListBox mostrata nella figura 13-1.

QListBox

Un'applicazione di esempio per QListBox

Figura 13-1 Un'applicazione di esempio per QListBox

QListBox e' il tipo di lista piu' semplice. Generalmente viene utilizzata per mostrare un'unica colonna con elementi testuali ma puo' essere utilizzata in situazioni piu' avanzate. Per mostrare una lista di elementi testuali si utilizzano i metodi insertItem oppure insertStringList. Per svuotare la lista si utilizza il metodo clear. Una dimostrazione e' data nell'esempio 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( "" );
}

Esempio 13-1

Il codice dell'esempio mostra l'implementazione di un widget che utilizza una QListBox. I puntatori m_listBox e m_lineEdit sono membri privati della classe. Notate come e' possibile connettere direttamente clear() a un segnale.

Ora sappiamo come popolare e svuotare una lista. Come facciamo a sapere quali elementi sono selezionati e come facciamo a sapere quando vengono selezionati? Una QListBox puo' avere tre tipi diversi di selezione: singola, multipla ed estesta.

E' anche disponibile una quarta modalita' che impedisce all'utente di selezionare qualcosa. La modalita' predefinita e' quella singola. Bene, ora come facciamo a sapere se c'e' un elemento selezionato e di quale elemento si tratta? Per fare una prova, aggiungiamo una label in fondo al nostro widget e uno slot che scrive sopra qual'e' la selezione corrente. Nel costruttore connettiamo il segnale selectionChanged al nostro slot. Il codice e' nell'esempio 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() );
}

Esempio 13-2

Provate l'esempio e giocate un po' con i pulsanti per generere segnali. Per esempio, notate come selectionChanged e' utilizzata al meglio con liste dal contenuto statico, dato che il segnale non viene emesso se la lista viene svuotata.

E' possibile sottoclassare QListBoxItem per avere delle liste piu' complesse. Un esempio e' dato da QListBoxPixmap e QjListBoxPixmap che rappresenta un QListBoxItem con testo e un'immagine. Il codice dell'esempio 13-3 mostra come impostare una list box con questo tipo di elementi al posto dei soliti con solo testo.

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" );

Esempio 13-3

Il risultato e' mostrato nella figura 13-2. Nel codice, osservate che gli elementi della list box vengono istanziati con un riferimento alla list box stessa. In questo modo essi si aggiungono automaticamente alla lista. Fate un confronto con il modo in cui si inseriscono elementi di solo testo. E' possibile utilizzare il metodo insertItem anche con gli elementi personalizzati. In questo caso, il codice assomiglierebbe allo slot addItem dell'esempio 13-1.

Una QListBox con QListBoxPixmaps

Figura 13-2 Una QListBox con QListBoxPixmaps

QListView

Quando si ha a che fare con dati piu' complessi, distribuiti su piu' colonne, conviene utilizzare il widget QListView al posto della QListBox. La QListView puo' mostrare dati su piu' colonne sotto forma di lista o di albero. Ogni colonna puo' avere un'intestazione per permettere agli utenti di ordinare i dati. La figura 13-3 mostra una schermata con l'applicazione dell'esempio.

Il programma di esempio

Figura 13-3 Il programma di esempio

Il widget list view e' piuttosto complesso, per cui l'esempio e' suddiviso in quattro parti, ognuna mostrata in un tab diverso nell'applicazione di esempio. Iniziamo con il caso piu' semplice - una banale lista con piu' colonne. Il tutto e' mostrato nel tab intitolato "List" e mostrato nella figura 13-3. Il codice per la list view e' mostrato nell'esempio 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;
}

Esempio 13-4

Il codice mostra i passi necessari per utilizzare una list view. Per iniziare, si crea una serie di colonne. Quindi si impostano le proprieta' che ci interessano. In questo caso, ci assicuriamo che la selezione venga mostrata su tutta la riga e non solo sull'elemento nella prima colonna. Infine, popoliamo la lista creando una serie di QListViewItem a cui passiamo un puntatore alla lista come parametro.

La rappresentazione ad albero

Figura 13-4 La rappresentazione ad albero

Nel secondo caso utilizziamo una struttura ad albero per mostrare gli elementi della lista. Il tutto e' mostrato nel tab intitolato "Tree". Il codice e' nell'esempio 13-5. Quando eseguite l'applicazione dell'esempio, provate a fare click sulle intestazioni delle colonne per ordinare gli elementi. Notate che ciascun ramo ("A", "B" e "C") viene ordinato indipendentemente.

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

Esempio 13-5

Il codice e' strutturato come nel caso della lista semplice. La differenza e' che al posto di usare il widget della listview come parent per tutti gli elementi, alcuni hanno altri elementi come parent. Alcuni elementi sono decorati con un "+" per indicare all'utente che ci sono altri elementi "figli" di quello.

Ordinare per colonna

Figura 13-5 Ordinare per colonna

Il terzo caso mostra un problema comune conscio alla maggior parte degli utenti windows. Se cercate di ordinare dei numeri che vengono trattati come stringhe, vi ritroverete con il dieci tra l'uno e il due. Il modo piu' veloce, ma non il piu' bello, di risolvere il tutto e' quello di riempire i numeri aggiungendo degli zeri nella parte sinistra o di trattare i numeri come cifre e non stringhe.

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

Esempio 13-6

L'esempio sull'ordinamento utilizza una sottoclasse personalizzata di QListViewItem mostrata nell'esempio 13-6. La sottoclasse reimplementa il metodo compare e tratta gli elementi della terza colonna come numeri e non testo. Le altre colonne utilizzano il metodo QListViewItem::compare predefinito.

Provate ad eseguire l'esempio come mostrato dalla figura 13-5. Cambiate l'ordinamento sulle varie colonne e osservate il risultato. Notate anche come ordinando la quarta colonna vengano prese in considerazione le minuscole e le maiuscole. Il codice per impostare la listview e' nell'esempio 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;
}

Esempio 13-7

Icone in una QListView

Figura 13-6 Icone in una QListView

Il tab "Icons" mostra come sia facile inserire delle icone in una listview. Possono esserci elementi diversi in colonne diverse e piu' icone su ciascuna riga. Il codice sorgente e' mostrato nell'esempio 13-8. Come potete osservare, aggiungere un'icona e' banale quanto un'invocazione del metodo 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;
}

Esempio 13-8

Lavorare con una list view e' molto simile a lavorare con una list box. Il segnale selectionChanged e' ancora disponibile, insieme ai metodi isSelected e setSelected. Nelle applicazioni moderne preferisco le list view al posto delle list box dato che le colonne hanno un'intestazione che fornisce all'utente ulteriori informazioni.

Uno dei problemi che si riscontrano lavorando con una lista e' la sincronizzazione tra i dati contenuti nella lista e quelli gestiti dall'applicazione. Per esempio, supponiamo che abbiate un'applicazione che gestisce un elenco telefonico. Ciascuna voce dell'elenco telefonico ha un'immagine e alcuni dettagli ad essa associati, come nome, numero di telefono, email, indirizzo, ecc. La list view che mostra i contatti non deve necessariamente mostrare tutte queste informazioni. Per esempio, finche' non si seleziona un elemento potrebbe essere sufficiente il solo nome. Ci sono diversi modi per affrontare questo problema:

  1. I contatti sono memorizzati in un database. Ciscun contatto ha un identificativo unico per individuarlo (in modo da poter avere piu' contatti con lo stesso nome). L'identificatore non viene mai mostrato ed esiste solo dietro le scene. La sincronizzazione puo' essere gestita inserendo questo identificatore in una colonna nascosta e utilizzandolo per recuperare il contatto da una QValueList o una QMap
  2. I contatti si trovano in memoria e sono memorizzati in una lista. Una list view speciale prende un contatto come input e, reimplementando il metodo text, estrae i dati necessari. Quando i dati cambiano, viene cambiato solo il contatto. Basta ridisegnare l'elemento della list view per mostrare i cambiamenti.
  3. Si sottoclassa QListViewItem reimplementando il metodo text. La nuova classe, ContactItem, contiene tutte le informazioni necessarie e inoltre puo' essere utilizzata nella list view. Il problema nasce se vogliamo una seconda vista dello stesso elemento.
  4. La sincronizzazione tra la lista con i contatti e gli elementi delle list view puo' essere incorporata nelle azioni di editing. Ciascuna azione deve preoccuparsi di modificare allo stesso modo sia il contatto che l'elemento della list view. La cosa puo' diventare molto complicata e si rischia di riempire eccessivamente il codice delle azioni.

Le prime tre soluzioni sono le mie preferite. La terza se si utilizza una sola vista, la seconda se tutti i dati sono in memoria e la prima se i dati sono troppi per essere mantenuti in memoria. Per implementare la prima soluzione e' necessaria una colonna nascosta. In base a questo consiglio, ciascuna colonna da nascondere deve avere la proprieta' "width mode" impostata su manuale. Il codice per nascondere la terza colonna della list view lv, compreso il cambiamento di "width mode", e' mostrato nell'esempio 13-9.

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

Esempio 13-9

QTable

Appena i dati diventano troppo strutturati e complessi, la list view e la list box potrebbero apparire troppo semplici. A questo punto diventa necessario l'utilizzo di una tabella. Con una QTable ogni cella della tabella puo' contenere un widget diverso. Per esempio checkbox, dropdown list o semplicemente testo.

E' semplice creare una tabella per poi popolarla con elementi utilizzando i metodi setText, setPixmap e setItem.

Quello che piu' importa e' in realta' la capacita' di creare elementi personalizzati. In questo modo e' possibile fornire agli utenti una migliore interazione. Per esempio, invece di selezionare il nome di un colore da un elenco, forniremo un elemento che mostri una lista di colori effettivi e che permetta all'utente di selezionarne uno. La figura 13-7 mostra l'applicazione completa.

Una QTable con ColorTableItems

Figura 13-7 Una QTable con ColorTableItems

Quello che abbiamo fatto e' sottoclassare QTableItem per creare una nuova classe, ColorTableItem. La dichiarazione della classe e' mostrata nell'esempio 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 );
};

Esempio 13-10

Osservando il codice qui sopra potete notare come i metodi interessanti siano paint, createEditor e setContentFromEditor. Gli ultimi due lavorano insieme mentre il primo puo' essere implementato indipendentemente dagli altri.

Il metodo paint prende il testo della cella assumendo che si tratti del nome di un colore. Quindi utilizza quel colore per riempire la cella. L'esempio 13-11 mostra l'implementazione del metodo 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()) );
}

Esempio 13-11

I metodi createEditor e setContentFromEditor lavorano insieme. Come prima cosa, createEditor crea il widget utilizzato per modificare quella data cella, quindi si recupera il risultato dal widget utilizzando il metodo setContentFromEditor. L'esempio 13-12 mostra il codice per createEditor. Viene creata una combo box con colori come elementi e ci si assicura anche che venga selezionato l'elemento giusto dalla lista.

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

Esempio 13-12

A questo punto, e come mostrato nell'esempio 13-13, setContentFromEditor non fa altro che prendere il widget che funge da editor per poi assicurarsi che si tratti di una combo box e inserire il risultato nella cella.

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

Esempio 13-13

Il codice per creare la tabella con gli elementi personalizzati e' nell'esempio 13-14. Osservate che affinche' il tutto funzioni, i nomi dei colori devono essere validi. Si puo' risolvere questo problema utilizzando una emumerazione o semplicemente 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" ) );

Esempio 13-14

La classe QTable presenta numerose limitazioni, motivo per cui si sono diffuse soluzioni di terze parti. Una di queste e' QicsTable della ICS, disponibile con due licenze diverse esattamente come Qt. La versione open source ha licenza GPL. Risulta molto piu' flessibile sotto ogni aspetto, dall'architettura model-view-controller alle celle unificabili, agli header piu' flessibili, alle righe e colonne bloccabili, alla possibilita' di stampare tabelle intere o solo parti di esse. Nel sito della ICS e' disponibile un documento intitolato Beyond QTable, che tratta le differenze tra QTable e QicsTable.

Qt 4 - Interview

In Qt 4, le classi QListView e QTable verranno in parte unite all'interno del framework Interview. L'architettura si basera' sulle viste e su un modello contenente i dati. In questo modo si riduce la necessita' di memorizzare i dati in piu' locazioni di memoria (p.es. come elemento di una list view e in una struttura dati dell'applicazione). Ulteriori informazioni su Qt 4 sono disponibili presso la Trolltech.

Sommario

Il codice per questo capitolo puo' essere scaricato qui.

  • Utilizzate una QListBox per liste semplice.
  • Utilizzate una QListView per liste piu' complesse e in cui potrebbero tornare utili gli header per le colonne.
  • Utilizzate una QTable per informazioni strutturate come tabella.
  • Non esitate a sottoclassare QListBoxItem, QListViewItem e QTableItem per creare elementi personalizzati. E' un approccio molto semplice per migliorare l'interfaccia utente.
  • Letture consigliate

    Qt Quarterly presenta un paio di articoli che riguardano l'argomento di questo capitolo: 1 e 2.

    This is a part of digitalfanatics.org.