Nel
precedente articolo abbiamo visto come personalizzare
il componente JTable, anche al fine di produrre un output
personalizzato per ciascuna cella. In questo articolo
vedremo invece come implementare un editor personalizzato.
Implementeremo infatti un campo di testo in grado di suggerire
il completamento di quanto si sta digitando, sulla base
degli esempi presenti in Mac OS X, ma anche in altri sistemi
operativi. Vedremo sostanzialmente due classi: JTableTest
non fa altro che creare una JTable di prova, mentre AutocompletingCellEditor
implementa un editor di cella.
La logica degli editor è simile a quella dei renderer
discussi nella puntata precedente.
Nel concreto, lesempio che vedremo simula una tabella
di gestione di un elenco di utenti e rispettivi gruppi
di lavoro, come si vede in Figura 1.
Figura 1 la JTable di prova
Una
tabella di prova
La classe JTableTest è la classe principale di
questo piccolo programma di prova e si occupa di istanziare
editor, tabella e finestra principale dellapplicazione.
Questa classe è dichiarata come segue:
package it.mokabyte.appdesktop.jtable;
import
javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
/**
* Programma di prova per verificare il funzionamento
degli
* editor di cella con il componente JTable
*
* @author max
*/
public class JTableTest {
Il
programma costruisce una tabella di utenti e rispettivi
gruppi di lavoro, che sono memorizzati in semplici vettori
di stringhe. Il vettore suggestions contiene tutti i
possibili valori per la colonna dei gruppi:
/**
elenco utenti */
String[] utenti = { "Max", "Giovanni",
"Andrea", "Marco" };
/** elenco dei gruppi relativi agli utenti */
String[] gruppi = { "user", "supervisor",
"user", "user" };
/** elenco dei possibli valori per i gruppi */
String[] suggestions = { "user", "guest",
"poker", "poweruser",
"supervisor", "god" };
Il
costruttore si occupa di costruire tutto quanto richiesto
dalloggetto (TableModel, JTable e JFrame). Il
modello, una sottoclasse anonima di AbstractTableModel,
non fa altro che mappare i vettori appena visti sulla
tabella, configurando una griglia di due colonne e di
un numero di righe pari al numero di elementi nel vettore
degli utenti. Limplementazione di questo modello
è volutamente molto semplice e non prevede controlli
sul superamento dei limiti dei vettori:
/**
* Crea questo oggetto
*/
public JTableTest() {
/*
* Modello dati di prova utilizzato dalla tabella
*/
AbstractTableModel model = new AbstractTableModel()
{
/**
* I nomi delle colonne sono fissi
* @return nome della colonna richiesta
*/
public String getColumnName(int col) {
switch( col ) {
case
0:
return
"Nome utente";
case
1:
return
"Gruppo di appartenenza";
}
return
"";
}
/**
* @return il numero di righe è calcolato
* sulla base dell'elenco degli utenti
*/
public int getRowCount() {
return
utenti.length;
}
/**
* @return numero di colonne (fisso 2)
*/
public int getColumnCount() {
return
2;
}
/**
* Questo metodo ritorna un valore booleano
* che indica se la
* cella è editabile. In questo specifico
* esempio è modificabile
* solo al seconda colonna.
*
* @return true se la cella è editabile
*/
public boolean isCellEditable(int row, int col) {
return
(col == 1);
}
/**
* Ottiene il valore di una cella, estraendo
* i dati direttamente
* dai vettori utenti e gruppi.
*
* @return valore della cella
*/
public Object getValueAt(int row, int col) {
switch(
col ) {
case
0:
return
utenti[ row ];
case
1:
return
gruppi[ row ];
}
return
"";
}
/**
* Imposta il valore della cella, metodo
* indispensabile per modificare
* i dati alla base del modello e vedere effettivamente
* applicate le modifiche inserite in fase di
* modifica delle celle.
*/
public void setValueAt(Object value, int row, int col)
{
gruppi[
row ] = (String)value;
}
};
//crea la tabella
JTable table = new JTable(model);
//ottiene un oggetto che rappresenta una colonna ed
imposta
//l'editor personalizzato
TableColumn tb = table.getColumnModel().getColumn(1);
tb.setCellEditor( new AutocompletingCellEditor(suggestions)
);
//crea una finestra di prova all'interno della quale
inserisce
//la tabella appena creata
JFrame frame = new JFrame("Test JTable");
frame.getContentPane().add(new JScrollPane(table));
frame.pack();
frame.setVisible(true);
}
Si noti la presenza dei metodi setValueAt() e isCellEditable(),
indispensabili per rendere editabile una tabella e modificare
i dati sottostanti. In questo caso è modificabile
solo la seconda colonna. Un altro particolare è
il modo di impostare leditor. Sebbene sulla classe
JTable siano presenti un paio di versioni del metodo
setCellEditor(), alcune prove di utilizzo di questo
metodo non hanno prodotto il risultato sperato. Invece,
impostando leditor su una colonna specifica, passando
dalloggetto TableColumn ha funzionato subito correttamente.
Come si vede, leditor passato al metodo setCellEditor()
è una istanza della classe AutocompletingCellEditor,
che vedremo tra breve.
La classe si conclude con il metodo main():
/**
*
*/
public static void main(String[] args) {
new
JTableTest();
}
}
Un
campo di testo autocompletante
La seconda classe del nostro esempio deriva da AbstractCellEditor
ed implementa TableCellEditor. Eccone il codice sorgente:
/**
* AutocompletingCellEditor
*/
package it.mokabyte.appdesktop.jtable;
import
java.awt.Component;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import
javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.table.TableCellEditor;
/**
* Implementa un editor di cella con autocompletamento
dei dati
* sulla base di un elenco di possibili suggerimenti.
Ovviamente
* si potrebbe implementare direttamente su un JTextField
e rendere
* quindi questa funzionalità disponibile anche
sui singoli campi
* di testo.
*
* @author max
*/
public class AutocompletingCellEditor extends AbstractCellEditor
implements TableCellEditor {
Il
componente utilizzato per gestire linput dellutente
è un semplice JTextField. Nella classe sono presenti
anche campi per contenere lelenco delle scelte
possibili ed il prefisso digitato dallutente:
/**
componente di modifica */
JTextField editorComponent;
/** elenco di scelte possibili */
String[] choices;
/** prefisso su cui elaborare un suggerimento */
String prefix;
Dal
costruttore viene invocato il metodo createUI(), che
si occupa di inizializzare il campo di testo:
/**
* Crea un nuovo oggetto impostando un vettore di possibili
* valori per questo campo di testo.
*
* @param suggestions
*/
public AutocompletingCellEditor(String[] suggestions)
{
this.choices
= suggestions;
createUI();
}
/**
* Crea l'interfacciautente costruendo il campo di testo
* JTextField su cui viene impostato un KeyListener.
* Questi eventi permettono di identificare le modifiche
* apportate dall'utente al campo di testo e di gestire
* anche il tasto backspace, che ha l'effetto di cancellare
* la selezione attiva.
*/
private void createUI() {
editorComponent
= new JTextField();
editorComponent.setBorder(
null );
editorComponent.addKeyListener(
new KeyAdapter() {
public
void keyTyped(KeyEvent e) {
//System.out.println(e);
if
(e.getKeyChar() == KeyEvent.VK_BACK_SPACE) {
deleteSelection();
}
else {
suggest();
}
}
}
);
}
Attenzione
al gestore di evento KeyListener: serve a controllare
linput dellutente man mano che lo digita
ed a gestire casi particolari come il tasto backspace.
Se infatti alla digitazione di ciascun carattere il
sistema risponde producendo nel campo di testo lelemento
del vettore choices più prossimo a quando digitato
dallutente, il tasto backspace ha invece leffetto
di cancellare quanto proposto dal sistema e quanto digitato
dallutente. In sostanza, quando si preme il primo
tasto nel campo di testo vuoto, il sistema presenta
lelemento che inizia per il carattere digitato.
In questo caso il testo selezionato nella casella di
testo è quello prodotto dal sistema. Proseguendo
con la digitazione, il sistema raffina la risposta,
proponendo la scelta che più si avvicina a quanto
digitato. Nel frattempo, quanto digitato è presente
nel campo prefix. La parte proposta dal sistema è
sempre lunica parte selezionata, anche se è
ovviamente possibile fare Ctrl + A (o Mela + A) per
selezionare tutto il contenuto del testo.
Quando si è nel caso in cui la selezione di testo
evidenzia quanto proposto dal sistema, premendo backspace
la prima volta viene cancellato quanto selezionato,
mentre proseguendo la digitazione del tasto backspace
si prosegue a cancellare anche il testo non selezionato.
Il tasto backspace è gestito dal seguente metodo:
/**
* Questo metodo viene invocato a seguito della pressione
* del tasto backspace, e comporta la cancellazione dal
campo di
* testo del testo selezionato. Si noti la presenza dell'invocazione
* dei metodi setSelectionStart() e setSelectionEnd()
che
* inizializzano lo stato di selezione del campo di testo.
*/
private void deleteSelection() {
int selectionStart = editorComponent.getSelectionStart();
String text = editorComponent.getText();
text = text.substring(0, selectionStart);
System.out.println( "deleteSelection(): selectionStart="
+ selectionStart );
System.out.println( "deleteSelection(): text="
+ text );
editorComponent.setSelectionStart(0);
editorComponent.setSelectionEnd(0);
editorComponent.setText(text);
}
La
ricerca di un suggerimento viene invece implementata
nel metodo suggest():
/**
* Determina un suggerimento per il valore attuale del
testo
* e lo presenta nel campo di testo.
*/
private void suggest() {
SwingUtilities.invokeLater( new Runnable() {
public void run() {
suggest( findSuggestion() );
}
});
}
La
ricerca di un suggerimento avviene con una ricerca sequenziale:
/**
* Individua un suggerimento sulla base del testo presente
* attualmente nel campo di modifica. Per fare questo
estrae
* quanto effettivamente digitato dall'utente, prendendo
la
* parte iniziale del contenuto testuale del JTextField
* utilizzato, fino all'inizio della selezione. Quest'ultima
* identifica infatti l'inizio della parte "suggerita"
dal
* sistema.
*
* La ricerca del suggerimento è poi sequenziale,
nel caso
* esista più di un valore rispondente allo stesso
prefisso
* viene preso il primo.
*
* @return il suggerimento da visualizzare
*/
private String findSuggestion() {
prefix = editorComponent.getText();
int selectionStart = editorComponent.getSelectionStart();
if (selectionStart != -1) {
prefix = prefix.substring(0, selectionStart);
}
System.out.println( "findSuggestion(): prefix="
+ prefix );
if (prefix.length() > 0) {
for( int i=0; i<choices.length; i++ ) {
if (choices[i].startsWith(prefix)) {
return choices[i];
}
}
}
return "";
}
Il
metodo suggest(String) è quello che si occupa
di valorizzare il campo di testo con il suggerimento
individuato nei passi precedenti:
/**
* Imposta un suggerimento nel campo di testo avendo
* l'accortezza di impostare la selezione sulla parte
non
* digitata dall'utente, identificata dal fatto che non
è
* presente nella stringa prefix.
*
* @param suggestion
*/
private void suggest( String suggestion ) {
System.out.println( "suggest(): suggestion="
+ suggestion );
if (suggestion.length() > 0 ) {
editorComponent.setText( suggestion );
editorComponent.setSelectionStart( prefix.length() );
editorComponent.setSelectionEnd( suggestion.length()
);
}
}
La
classe termina con un paio di metodi di supporto necessari
allinterfacciamento con JTable. Il primo permette
di restituire il valore della cella, così come
modificato dallutente. Il secondo viene utilizzato
da JTable per ottenere il componente di editing. Allinterno
di questo metodo solitamente si manipola lo stato delloggetto
in funzione di diversi parametri, come ad esempio in
funzione dello stato di selezione della cella:
/**
* @return il valore di ritorno dell'editor di cella
*/
public Object getCellEditorValue() {
return editorComponent.getText();
}
/**
* Questo metodo è specificato nell'interfaccia
TableCellEditor
* e viene utilizzato da JTable per richiedere un componente
da
* utilizzare per la modifica della cella ogni qual volta
questa
* operazione si renda necessaria.
*
* @return componente di modifica della cella
*/
public Component getTableCellEditorComponent(JTable
table,
Object value, boolean isSelected, int row, int col)
{
editorComponent.setText((String)value);
return editorComponent;
}
}
Conclusioni
In questo articolo abbiamo visto brevemente come implementare
un editor di celle personalizzato. Ovviamente la funzionalità
di autocompletamento può essere implementata
direttamente in una sottoclasse di JTextField, ed essere
impiegata in altri ambiti.Il refactoring richiesto è
abbastanza semplice.
Questo esempio ci ha permesso di vedere però
come JTable gestisce gli editor, ed abbiamo scoperto
che sono sostanzialmente speculari ai renderer illustrati
nel numero precedente.
Allegati
Scarica
gli esempi allegati
all'articolo
|