MokaByte 103 - Gennaio 2006
 
MokaByte 103 - Gennaio 2006 Prima pagina Cerca Home Page

 

 

 

Applicazioni Desktop
La misteriosa JTable

Nelle precedenti puntate abbiamo visto come utilizzare Swing in modo più avanzato per gestire finestre delle preferenze. Un elemento che si può trovare in queste finestre e non solo sono le tabelle. In Swing il loro utilizzo è abbastanza semplice ma la personalizzazione più avanzata richiede qualche nozione in più.

Le tabelle sono uno dei componenti visuali più utilizzati per realizzare interfacce utente non banali. Sono molto utilizzate in ambito gestionale, ma se ne è vista una certa proliferazione anche nelle funzionalità di base dei sistemi operativi, da Windows a Linux fino ad arrivare a Mac OS X. La classe JTable, presente sotto il package javax.swing è una implementazione abbastanza ricca, che permette di personalizzare molteplici aspetti della visualizzazione. JTable è però anche uno degli elementi più complessi di Swing, sebbene uno di quelli più necessari. La classe JTable infatti non esaurisce gli oggetti coinvolti nelle tabelle, in quanto è presente un package dedicato: javax.swing.table.

Partire dalle basi
Costruire una semplice tabella non è affatto complesso: è sufficiente creare un oggetto di tipo TableModel, ad esempio creando una sottoclasse di AbstractTableModel ed implementare tre metodi:

  • getColumnCount() ritorna il numero di colonne della tabella;
  • getRowCount() ritorna il numero di righe della tabella;
  • getValueAt() ritorna il valore di una cella della tabella.

L’esempio classico di codice, presente nella documentazione JavaDoc, è il seguente:


TableModel dataModel = new AbstractTableModel() {
public int getColumnCount() { return 10; }
public int getRowCount() { return 10;}
public Object getValueAt(int row, int col) { return new Integer(row*col); }};
JTable table = new JTable(dataModel);
JScrollPane scrollpane = new JScrollPane(table);

L’oggetto TableModel è il modello dei dati della tabella. Swing infatti suddivide la funzionalità del componente visuale dai dati secondo una versione modificata del pattern MVC. La classe JTable implementa la parte VC del nome (View-Controller), mentre l’oggetto TableModel implementa la parte M (Model).
Nell’esempio di codice il contenuto di ciascuna cella è generato dinamicamente, ma in altri casi potrebbe essere preso dal database o dal modello ad oggetti del sistema.
Questa semplice tabella ha però alcune limitazioni importanti:

  • non è presente il nome di colonna.
  • Tutte le righe hanno lo stesso sfondo: in caso di un elenco molto lungo la leggibilità si abbassa. In alcune interfacce utente si sceglie di differenziare il colore di sfondo per rendere l’esempio più leggibile.
  • La tabella distribuisce lo spazio alle colonne in modo proporzionale alla dimensione dei dati senza considerare il risultato grafico finale.
  • È necessario formattare il dato nel metodo getValueAt(). Per formattazione si intende, ad esempio, l’utilizzo di separatori nei numeri oppure l’uso del formato italiano per le date.
  • Non è direttamente supportato l’ordinamento per colonna.
    In realtà non sarebbe disponibile neanche lo scrolling, che viene però ottenuto inserendo l’oggetto JTable in una JScrollPane. Si noti che se si inserisce una JTable direttamente in un contenitore senza passare per la JScrollPane, non verranno visualizzate le intestazioni di colonna.

Arriva iAuthor
Per approfondire le funzionalità di JTable verrà illustrata una ipotetica applicazione che la redazione di Mokabyte potrebbe utilizzare per gestire l’elenco degli articoli pubblicati. Il risultato che si desidera ottenere è illustrato in Figura 1.


Figura 1
– Elenco di articoli per un autore

Questa finestra ha alcune caratteristiche evidenti ed alcune meno:
il colore delle linee è alternato.
Le colonne battute e data consegna sono formattate opportunamente.
È presente una intestazione di tutte le colonne. La larghezza delle colonne è impostata da programma. L’allineamento delle celle di testo è a sinistra, mentre quelle numeriche a destra.
Facendo clic su una colonna la tabella viene ordinata per quella colonna.
Facendo doppio clic su una riga viene aperta la finestra di modifica dell’articolo selezionato.
La funzionalità forse più semplice da implementare è l’intestazione di colonna, che richiede solo l’implementazione di un metodo:

public String getColumnName(int col) {
  if (col == 0) {
    return "Titolo";
  }
  if (col == 1) {
    return "Battute";
  }
  if (col == 2) {
    return "Editore";
  }
  if (col == 3) {
    return "Testata";
  }
  if (col == 4) {
    return "Data consegna";
  }
}

Non è neanche troppo difficile impostare la larghezza di una colonna. È sufficiente ottenere l’oggetto TableColumn relativo alla colonna desiderata ed impostare il valore della proprietà preferredWidth. Per ottenere una colonna si utilizza il metodo getColumn() che si aspetta come parametro il nome della colonna o un progressivo:

TableColumn c = table.getColumn("Titolo");
c.setPreferredWidth( 200 );

 

Intercettare i clic sulle colonne
Per ordinare la tabella per colonna è necessario implementare l’algoritmo di ordinamento nel proprio codice. Solitamente sono possibili due alternative: utilizzare il metodo sort() della classe Arrays per ordinare un vettore di primitive o oggetti, oppure, se i dati arrivano dal database, ordinarli attraverso la clausola ORDER BY. Il problema è quindi quello di intercettare il clic sulla colonna. Per farlo è necessario aggiungere un MouseListener all’oggetto JTableHeader, che rappresenta tutta la riga di intestazioni della tabella. Il gestore di eventi può essere una sottoclasse di MouseAdapter in quanto è necessario implementare solo il metodo mouseClicked(), che viene invocato quando l’utente fa clic sull’intestazione della tabella.
Questo metodo si aspetta un oggetto MouseEvent da cui è possibile capire la colonna selezionata estraendo le coordinate di clic tramite i metodi getX() e getY(). Dalle coordinate si ottiene la colonna utilizzando il metodo columnAtPoint() della classe JTable, a cui passare un oggetto Point inizializzato con le coordinate precedentemente ottenute. Questo metodo ritorna l’indice della colonna, che può dunque essere utilizzato per ordinare i dati opportunamente:

final JTableHeader tableHeader = getTableHeader();
tableHeader.addMouseListener( new MouseAdapter() {
  public void mouseClicked(MouseEvent e) {
    int x = e.getX();
    int y = e.getY();
    int columnIndex = tableHeader.columnAtPoint( new Point(x,y) );
    sort( columnIndex );
  }
} );

In questo codice, l’ordinamento avviene nel metodo sort(). L’implementazione di questo metodo non fa altro che ottenere un nuovo TableModel, che viene associato all’oggetto JTable attraverso il metodo setModel(). Questa operazione rinfresca la vista aggiornando il contenuto della JTable.

 

Manipolare la visualizzazione
A questo punto è possibile passare alla manipolazione degli elementi presenti nella tabella. La visualizzazione, ma anche la modifica, dei dati delle celle è eseguita utilizzando un oggetto Renderer. In particolare, una classe che implementa l’interfaccia TableCellRenderer. JTable dispone di una implementazione di riferimento, che è possibile sostituire utilizzando il metodo setDefaultRenderer(). La classe ElencoArticoliTableCellRenderer illustrata nel listato seguente svolge due attività:

  • Alterna i colori di sfondo delle righe.
  • Formatta i dati di tipo intero utilizzando un DecimalFormat apposito.

Il metodo getTableCellRendererComponent(), definito nell’interfaccia TableCellRenderer, deve fornire un componente Swing opportunamente configurato per visualizzare il dato della cella corrente. I parametri di questo metodo sono:

  • la tabella JTable che ha generato la richiesta. Questo permette di condividere più renderer in diverse tabelle.
  • Il valore da visualizzare, sottoforma di Object.
  • Un flag booleano che indica se la cella è selezionata.
  • Un flag booleano che indica se la cella ha il fuoco.
  • La riga della cella.
  • La colonna della cella.

Queste informazioni permettono di identificare univocamente la cella in oggetto, il suo valore ed eventualmente di aggiustare il componente di visualizzazione per riflettere uno stato di selezione della riga (quindi magari con colore di sfondo diverso) oppure di posizione del cursore.
La nostra implementazione di questo metodo inizia creando un oggetto JLabel, che sarà utilizzato per tutte le celle. Poi viene controllato se il tipo di dato passato è di tipo Integer. In questo caso viene applicata una formattazione personalizzata tramite DecimalFormat e impostato l’allineamento a destra (utilizzando una costante della classe SwingConstants). Per tutti gli altri tipi di dati il contenuto dell’etichetta è il risultato della chiamata di toString() sull’Object che rappresenta il valore passato come parametro. Il nostro TableModel prevede solo una colonna di tipo Integer, quella che contiene il numero di battute. Se avessimo avuto altre colonne di questo tipo, avrebbero avuto anch’esse quel tipo di formattazione, che sostanzialmente rappresenta un numero intero con separatori di migliaia. In casi più complessi è possibile utilizzare il parametro col per capire di che colonna si tratta, in modo da applicare eventualmente formattazioni differenti.
Questa prima parte del metodo esaurisce la parte di formattazione della colonna con la gestione dell’allineamento del contenuto a destra o a sinistra e la formattazione dei numeri. Una nota: l’unica colonna data viene formattata, nel TableModel utilizzato per generare la Figura 1, direttamente nel codice. Questo approccio misto è sconsigliabile: meglio raccogliere le formattazioni solo nel proprio renderer. In questo caso la colonna dovrebbe essere di tipo Date.
La seconda parte del metodo si occupa della gestione dei colori. Per prima cosa, viene impostata l’opacità a true, in modo che la JLabel disegni anche lo sfondo. Se infatti si imposta il colore di sfondo di questo oggetto, ma l’opacità è impostata a false, lo sfondo non viene disegnato, rimanendo del colore di default della tabella.
In seguito, il primo controllo è legato allo stato di selezione della cella. In questo caso il testo è bianco su blu. Quest’ultima tonalità di colore, definita dalla costante SELECTED_COLOR. Per definire i colori da utilizzare è stata analizzata l’interfaccia utente di iTunes, che ha una tabella simile a quella che stiamo creando.
Se la cella non è selezionata, viene impostato il colore del testo con quello di default della tabella. Poi viene determinato se il numero di riga è pari o dispari, ed impostato di conseguenza il colore dello sfondo con il valore di ALTERNATE_COLOR, oppure dal colore di sfondo di default della tabella. I colori di default della tabella riflettono quelli dell’interfaccia utente Aqua.
Il codice è il seguente:

package it.bigatti.iauthor.views;

import it.bigatti.iauthor.util.Formats;

import java.awt.Color;
import java.awt.Component;

import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.SwingConstants;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;

public class ElencoArticoliTableCellRenderer extends DefaultTableCellRenderer {
  private static final Color SELECTED_COLOR = new Color(61,128,223);
  private static final Color ALTERNATE_COLOR = new Color(232,242,254);

  public Component getTableCellRendererComponent(
    JTable table, Object value, boolean isSelected,
    boolean hasFocus, int row, int col) {
      JLabel testLabel;
      if (value instanceof Integer) {
        Integer intValue = (Integer)value;
        String s = Formats.numberFormat.format( intValue.intValue() ) + " ";
        testLabel = new JLabel(s, SwingConstants.RIGHT);
      } else {
        testLabel = new JLabel( value.toString() );
      }
      testLabel.setOpaque(true);
      if (isSelected) {
        testLabel.setForeground(Color.WHITE);
        testLabel.setBackground(SELECTED_COLOR);
      } else {
        testLabel.setForeground(table.getForeground());
        if (row % 2 == 0) {
          testLabel.setBackground(ALTERNATE_COLOR);
        } else {
          testLabel.setBackground(table.getBackground());
        }
      }
      return testLabel;
    }
}


Si noti che non è stato preso in considerazione lo stato del “fuoco” in quanto la tabella è considerata un insieme di righe e non di celle. In altre tabelle, più simili ad un foglio elettronico, potrebbe nascere la necessità di evidenziare la cella attiva. In questo caso la sua evidenziazione è stata volutamente omessa per evidenziare la gestione a righe e uniformare maggiormente l’interfaccia ad Aqua.
Un ulteriore configurazione è stata fatta invocando il metodo setColumnSelectionAllowed(false) in modo da disabilitare la selezione di singole colonne. Ancora una volta, è stata scelta questa strada per uniformità con l’interfaccia Aqua. In applicazioni come i fogli elettronici si potrebbe voler impostare questa proprietà a true.
Un ultima osservazione sugli oggetti Renderer. La firma del metodo che permette di impostare un renderer su una tabella è:

void setDefaultRenderer(Class, TableCellRenderer)

Il primo parametro permette di indicare per quale tipo di dato è valido il renderer. Nel nostro caso è stato utilizzato un solo renderer per tutti i tipi di dati, infatti è stato passato Object. Se avessimo voluto separare la gestione dei dati String, Integer o Date avremmo potuto passare renderer diversi. Ad esempio, per impostare un renderer specifico per le date si può scrivere:

table.setDefaultRenderer(Date.class, new DateRenderer());

 

Conclusioni
In questo articolo abbiamo approfondito l’uso di JTable, considerandola più uno strumento di visualizzazione e selezione che di modifica. In questo secondo caso si sarebbe dovuto utilizzare altre funzionalità di JTable, come gli editor personalizzabili.
Il loro funzionamento è simile ai renderer: è presente una interfaccia da implementare, TableCellEditor ed è presente un metodo su JTable per associare un editor ad un tipo di dato: setDefaultEditor(). Queste problematiche potrebbero, perché no, essere argomento di un prossimo articolo.