MokaByte 104 - Febbraio 2006
 
MokaByte 104 - Febbraio 2006 Prima pagina Cerca Home Page

 

 

 

Applicazioni Desktop
Ancora su 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ù.

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, l’esempio 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 dell’applicazione. 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 dall’oggetto (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. L’implementazione 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 l’editor. 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 l’editor su una colonna specifica, passando dall’oggetto TableColumn ha funzionato subito correttamente. Come si vede, l’editor 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 l’input dell’utente è un semplice JTextField. Nella classe sono presenti anche campi per contenere l’elenco delle scelte possibili ed il prefisso digitato dall’utente:

/** 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 l’input dell’utente 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 l’elemento del vettore choices più prossimo a quando digitato dall’utente, il tasto backspace ha invece l’effetto di cancellare quanto proposto dal sistema e quanto digitato dall’utente. In sostanza, quando si preme il primo tasto nel campo di testo vuoto, il sistema presenta l’elemento 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 l’unica 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 all’interfacciamento con JTable. Il primo permette di restituire il valore della cella, così come modificato dall’utente. Il secondo viene utilizzato da JTable per ottenere il componente di editing. All’interno di questo metodo solitamente si manipola lo stato dell’oggetto 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