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.
Lesempio
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);
Loggetto
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 loggetto TableModel
implementa la parte M (Model).
Nellesempio 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 lesempio 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, lutilizzo
di separatori nei numeri oppure luso del formato
italiano per le date.
- Non
è direttamente supportato lordinamento
per colonna.
In realtà non sarebbe disponibile neanche lo
scrolling, che viene però ottenuto inserendo
loggetto 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 lelenco
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.
Lallineamento 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 dellarticolo selezionato.
La funzionalità forse più semplice da
implementare è lintestazione di colonna,
che richiede solo limplementazione 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 loggetto
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 lalgoritmo 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 alloggetto
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 lutente fa clic sullintestazione
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 lindice 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, lordinamento avviene nel metodo
sort(). Limplementazione di questo metodo non
fa altro che ottenere un nuovo TableModel, che viene
associato alloggetto 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 linterfaccia 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 nellinterfaccia
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 lallineamento a destra (utilizzando
una costante della classe SwingConstants). Per tutti
gli altri tipi di dati il contenuto delletichetta
è il risultato della chiamata di toString() sullObject
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
anchesse 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 dellallineamento
del contenuto a destra o a sinistra e la formattazione
dei numeri. Una nota: lunica 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 lopacità
a true, in modo che la JLabel disegni anche lo sfondo.
Se infatti si imposta il colore di sfondo di questo
oggetto, ma lopacità è 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. Questultima tonalità
di colore, definita dalla costante SELECTED_COLOR. Per
definire i colori da utilizzare è stata analizzata
linterfaccia 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 dellinterfaccia
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 linterfaccia
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 linterfaccia 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 luso 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.
|