Il
problema che si desidera affrontare e risolvere è
quello di implementare la persistenza ed il recupero
della configurazione che è possibile impostare
nella finestra delle preferenze. Questa può includere
diversi componenti visuali, di tipi diversi. Si possono
avere infatti checkbox, combobox, tabelle, campi di
testo. Ciascuno di questi elementi permette di configurare
un aspetto dellapplicazione. La finestra che prenderemo
in considerazione è illustrata in Figura 1.
Figura 1 La finestra delle preferenze
presa in esame
Come si può notare, sono presenti diversi checkbox,
combobox e campi di testo. Per ciascuno di questi è
necessario, alla chiusura della finestra, memorizzare
il contenuto da qualche parte. Alla sua apertura, invece,
è necessario recuperare linformazione salvata
ed inizializzare i componenti visuali opportunamente.
Essendo componenti di tipo diverso, è necessario
trattare ciascuno di questi in modo specifico: per le
checkbox è necessario salvare un valore booleano,
per i campi di testo il contenuto testuale e per i combobox
lelemento selezionato. Per altri elementi, come
liste o tabelle, sarebbe necessario supportare diverse
modalità. Inoltre, è necessario notare
che ciascuno di questi componenti visuali ha una interfaccia
diversa. Per ciascuno è presente un diverso metodo
che estrae il valore corrente.
Loggetto
Preferenze
Tra i diversi possibili approcci per la soluzione della
problematica che ci siamo posti, invece che implementare
i classici pezzi di codice che prendono e impostano
i valori attuali dei singoli componenti, si è
tentato un nuovo approccio. Si è pensato al concetto
di collegare i componenti visuali ad un identificativo.
Ad esempio, il campo di testo emailTextField, che contiene
lindirizzo e-mail del mittente, viene associato
allidentificativo ID_EMAIL (una costante che vale
emailAccount). Quando tutti i componenti
visuali che contengono elementi di configurazione sono
collegati è possibile invocare il metodo che
esegue la persistenza, che non farà altro che
scorrere tutti i collegamenti e salvare ciascun ID con
il valore del relativo componente. In questo modo si
ottengono una serie di vantaggi:
- E'
necessario meno codice personalizzato per eseguire
persistenza e recupero della configurazione.
- Non
è necessario porre attenzione al tipo di componente
visuale che si sta memorizzando o recuperando, perché
di questo si occupa la classe che gestisce la persistenza.
- Sempre
per lo stesso motivo, un cambiamento nella GUI utilizzata,
e di conseguenza del componente visuale utilizzato,
non richiede modifica al codice di persistenza, a
meno che ovviamente il tipo del nuovo componente non
sia supportato dalla classe che gestisce la persistenza.
In questo caso però sarà sufficiente
estendere questultima per supportare il nuovo
componente. In questo modo, automaticamente, la nuova
tipologia sarà disponibile in tutte le finestre
di preferenze che utilizzano la classe base.
Per
gestire la persistenza delle preferenze è stata
creata la nuova classe Preferenze, che contiene tre
attributi:
- Il
nome del file su cui memorizzare la configurazione.
- I
collegamenti tra componenti visuali e gli ID
- I
valori di default, se presenti, per ciascun ID.
I
collegamenti ed i valori di default sono memorizzati
in Map dove la chiave è lID, di tipo String,
mentre il valore contiene, nel primo caso, loggetto
Swing utilizzato, nel secondo un oggetto che rappresenta
il valore di default.
La classe prevede un costruttore di default ed uno che
si aspetta il nome del file, oltre ad un metodo per
impostare questultimo in un secondo tempo.
/**
* Classe generica di gestione delle preferenze
* utente
* @author max
*/
public abstract class Preferenze {
private String filename;
private Map bindings = new HashMap();
private Map defaultValues = new HashMap();
public Preferenze() {
}
public Preferenze( String filename ) {
setFilename(filename);
}
public void setFilename( String filename
) {
this.filename = filename;
}
La
classe definisce anche il metodo astratto getDescrizione()
che deve essere implementato nelle sottoclassi di Preferenze
per fornire una descrizione da associare al file di
configurazione:
/**
* Le sottoclassi forniscono la descrizione
* del file di configurazione
* @return descrizione del file di configurazione
*/
public abstract String getDescrizione();
Il
resto dellimplementazione prevede due versioni
del metodo bind(), che permette di collegare un JComponent
con un ID di tipo String. La seconda versione si aspetta
anche un valore di default:
/**
* Collega un componente ad un elemento di configurazione
* @param component
* @param id
*/
public void bind( JComponent component, String id )
{
bindings.put( id, component );
}
/**
* Valore di default
* @param component
* @param id
* @param defaultValue
*/
public void bind( JComponent component, String id, Object
defaultValue ) {
bind( component, id );
defaultValues.put( id, defaultValue );
}
Il
metodo che si occupa della memorizzazione della configurazione
si chiama store() e può sollevare una IOException,
nel caso ci siano problemi nella scrittura della configurazione.
Questo metodo crea una Map chiamata state che viene
utilizzata per contenere i valori di configurazione
ed itera per tutti i collegamenti. Per ciascun valore
viene estratto il componente Swing ed in base al suo
tipo viene estratto il valore attuale, che viene memorizzato
nello stato. Questo viene poi riversato in un oggetto
di tipo Properties. Questo viene poi scritto nel file
di configurazione in precedenza aperto attraverso un
oggetto FileOutputStream.
/**
* Memorizza la configurazione
* @throws IOException
*/
public void store() throws IOException {
checkState();
Map state = new HashMap();
for( Iterator iter = bindings.keySet().iterator();
iter.hasNext(); ) {
String id = (String)iter.next();
JComponent c = (JComponent)bindings.get(id);
if (c instanceof JCheckBox)
{
JCheckBox cc = (JCheckBox)c;
state.put( id, String.valueOf(cc.isSelected())
);
}
else
if (c instanceof JTextField) {
JTextField cc =
(JTextField)c;
state.put( id, cc.getText()
);
}
else if (c instanceof JComboBox)
{
JComboBox cc = (JComboBox)c;
String s = (String)cc.getSelectedItem();
state.put( id, s
);
}
}
FileOutputStream out = new FileOutputStream(filename);
Properties p = new Properties();
p.putAll(state);
p.store( out, getDescrizione() );
}
Il
metodo di recupero della configurazione è paritetico
e si chiama load(). Questo utilizza un oggetto Properties
che legge dal file di configurazione tramite un oggetto
di tipo FileInputStream. Il codice è il seguente:
/**
* Carica la configurazione
* @throws IOException
*/
public void load() throws IOException {
checkState();
Properties p = new Properties();
p.load( new FileInputStream(filename) );
for( Iterator iter = bindings.keySet().iterator();
iter.hasNext(); ) {
String id = (String)iter.next();
JComponent c = (JComponent)bindings.get(id);
String defaultValue = (String)defaultValues.get(id);
String value = p.getProperty(id,
defaultValue);
if (c instanceof JCheckBox)
{
JCheckBox cc = (JCheckBox)c;
cc.setSelected(
"true".equals(value) );
} else if (c instanceof JTextField)
{
JTextField cc =
(JTextField)c;
cc.setText(value);
} else if (c instanceof JComboBox)
{
JComboBox cc = (JComboBox)c;
cc.setSelectedItem(value);
}
}
}
Entrambi
questi metodi, per prima cosa, verificano che siano
presenti collegamenti, utilizzando il metodo checkState():
/**
* Verifica che siano stati eseguiti i collegamenti
* @throws IllegalArgumentException
*/
private void checkState() throws IllegalArgumentException
{
if (bindings.size() == 0) {
throw new IllegalArgumentException("E'
necessario prima fare bind");
}
}
Evolvere la classe base
Lapproccio mostrato ha due limitazioni:
- memorizza
la configurazione sempre e comunque su un file di
testo. Sebbene questo sia lo standard per le applicazioni
desktop (e quelle Mac OS X), si potrebbe desiderare
di memorizzare questi dati in un database relazionale,
un file XML oppure il registro di Windows. In questi
casi è necessario scorporare la gestione della
memorizzazione. Si può pensare anche di utilizzare
package open source come Jakarta Commons Configurations
che offre proprio questo tipo di funzionalità;
- laccesso
in lettura e scrittura sui diversi componenti Swing
utilizza un elenco inelegante e poco manutenibile
di if. Si può pensare di sostituire questa
parte con una interfaccia, implementazioni concrete
per ciascun componente ed una factory. Un esempio
di implementazione è il seguente:
Il
metodo load() non fa altro che richiedere un ComponentAccessor
alloggetto ComponentAccessorFactory e poi impostare
il valore recuperato dal file di configurazione:
public
void load() throws IOException {
checkState();
Properties p = new Properties();
p.load( new FileInputStream(filename) );
for( Iterator iter = bindings.keySet().iterator();
iter.hasNext(); ) {
String id = (String)iter.next();
JComponent c = (JComponent)bindings.get(id);
String defaultValue = (String)defaultValues.get(id);
String value = p.getProperty(id,
defaultValue);
ComponentAccessor ca = ComponentAccessorFactory.getComponent(c);
ca.setValue(value);
}
}
Linterfaccia
contiene un metodo per ottenere il valore ed uno per
impostarlo:
interface
ComponentAccessor {
String getValue();
void setValue(String value);
}
La
factory non fa altro che ritornare una istanza concreta
di tipo ComponentAccessor in funzione del tipo di parametri
(in realtà la fila di if è stata spostata
qui, ma è sempre possibile pensare ad utilizzare
la reflection per eliminare questa ultima dipendenza):
class
ComponentAccessorFactory {
public static ComponentAccessor getComponent(
JComponent component ) {
if (component instanceof JCheckBox)
{
return new CheckBoxComponentAccessor((JCheckBox)component);
}
//altri
throw new IllegalArgumentException("non
supportato");
}
}
Limplementazione
dei singoli ComponentAccessor è poi banale. Qui
di seguito è illustrata limplementazione
dellaccessor per loggetto JCheckBox:
class
CheckBoxComponentAccessor implements ComponentAccessor
{
JCheckBox checkBox;
public CheckBoxComponentAccessor(JCheckBox
checkBox) {
this.checkBox = checkBox;
}
public String getValue() {
return String.valueOf( checkBox.isSelected()
);
}
public void setValue(String value) {
checkBox.setSelected( "true".equals(value)
);
}
}
Il
secondo approccio è sicuramente più elegante
ed orientato ai pattern, ma richiede molto più
codice.
Preferenze
personalizzate
La classe Preferenze però non è sufficiente
per implementare la persistenza della finestra vista
in Figura 1. È necessario infatti creare una
sottoclasse che fornisca limplementazione del
metodo getDescrizione(). La classe SudokuPreferenze
qui illustrata include anche un costruttore ed un elenco
di costanti che definiscono gli ID di configurazione
utilizzati in questa applicazione.
import
java.io.File;
import javax.swing.*;
public
class SudokuPreferenze extends Preferenze {
/** nome del file di default */
private final String FILENAME = ".mySudoku.properties";
//pannello generale
public static final String ID_SHOW_TIMER = "showTimer";
public static final String ID_HIDE_WHEN_PAUSE = "hideWhenPause";
public static final String ID_PAUSE_WHEN_HIDE = "pauseWhenHide";
public static final String ID_SHOW_ERRORS = "showErrors";
public static final String ID_ALLOW_HINTS = "allowHints";
public static final String ID_USE_TIMER = "useTimer";
//pannello nuovo gioco
public static final String ID_GRID_SIZE = "gridSize";
public static final String ID_LEVEL = "level";
//pannello posta elettronica
public static final String ID_EMAIL = "emailAccount";
public static final String ID_EMAIL_NOME = "emailName";
public static final String ID_SMTP_SERVER = "smtpServer";
/**
* Inizializza costruendo il nome del file delle
* proprietà mettendo FILENAME nella user.home
*/
public SudokuPreferenze() {
String filename = System.getProperty("user.home")
+File.separator +FILENAME;
setFilename( filename );
}
/**
*
*/
public String getDescrizione() {
return "File di configurazione di mySudoku";
}
}
Si noti come la classe Preferenze e SudokuPreferenze
suddividano opportunamente le parti comuni, e quindi
riutilizzabili in altre applicazioni, e quelle specifiche
ad un determinato programma.
Modificare
la finestra delle preferenze
È possibile includere la gestione della memorizzazione
e del recupero delle preferenze direttamente nella classe
AbstractPreferencesFrame. Daltra parte, il suo
stesso nome indica come questa sia una finestra di preferenze,
dunque avrà sicuramente lesigenza di memorizzare
e recuperare la configurazione. Per questo motivo:
è stato aggiunto un attributo di tipo Preferenze.
Nel costruttore viene chiamato il metodo load() per
recuperare la configurazione, dopo che la GUI è
stata inizializzata e prima di visualizzare la finestra
allutente.
Viene installato un WindowListener che, quando lutente
chiude la finestra, chiama il metodo store() definito
in seguito. Su Windows lo standard è avere un
pulsante di salvataggio (OK), mentre su Mac OS X (la
finestra riprende, per gioco, questo sistema operativo)
è sufficiente chiudere la finestra.
Sono stati aggiunti i metodi load() e store(), che non
fanno altro che richiamare i metodi omonimi sulloggetto
Preferenze.
Il codice è il seguente:
public
abstract class AbstractPreferencesFrame extends JFrame
{
//...
/** oggetto preferenze collegato alla finestra
*/
protected Preferenze preferenze;
public AbstractPreferencesFrame( String
title, int panelCount ) {
//...
//carica la configurazione
try {
load();
} catch (IOException e) {
e.printStackTrace();
}
//aggancio salvataggio preferenze alla chiusura
della finestra.
addWindowListener( new WindowAdapter() {
public void windowClosing(WindowEvent
event) {
try {
store();
} catch (IOException
e) {
e.printStackTrace();
}
setVisible(false);
dispose();
}
} );
}
/**
* Carica la configurazione
* @throws IOException
*/
protected void load() throws IOException
{
preferenze.load();
}
/**
* Salva la configurazione
* @throws IOException
*/
protected void store() throws IOException
{
preferenze.store();
}
Il tocco finale si trova nella classe PreferencesFrame,
che rappresenta la finestra delle preferenze concreta
per il gioco del Sudoku e che contiene tutti gli elementi
GUI presenti nella Figura 1. Il metodo bind() esegue
il collegamento tra i singli componenti Swing e gli
ID:
private
void bind() {
preferenze = new SudokuPreferenze();
//pannello generale
preferenze.bind(useTimerCheckBox, SudokuPreferenze.ID_USE_TIMER);
preferenze.bind(showTimerCheckBox, SudokuPreferenze.ID_SHOW_TIMER);
preferenze.bind(hideWhenPauseCheckBox, SudokuPreferenze.ID_HIDE_WHEN_PAUSE);
preferenze.bind(pauseWhenHideCheckBox, SudokuPreferenze.ID_PAUSE_WHEN_HIDE);
preferenze.bind(showErrorsCheckBox, SudokuPreferenze.ID_SHOW_ERRORS);
preferenze.bind(allowHintsCheckBox, SudokuPreferenze.ID_ALLOW_HINTS);
//pannello nuovo gioco
preferenze.bind(gridSizeComboBox, SudokuPreferenze.ID_GRID_SIZE);
preferenze.bind(levelComboBox, SudokuPreferenze.ID_LEVEL);
//pannello posta elettronica
preferenze.bind(emailTextField, SudokuPreferenze.ID_EMAIL);
preferenze.bind(nomeTextField, SudokuPreferenze.ID_EMAIL_NOME,
System.getProperty("user.name"));
preferenze.bind(smtpServerTextField, SudokuPreferenze.ID_SMTP_SERVER);
}
Conclusioni
In questo articolo abbiamo visto un possibile approccio
alla persistenza della configurazione dellapplicazione.
Sebbene migliorabile, mostra un modo comodo per leggere
e salvare lo stato di componenti Swing.
|