Nell'articolo
precedente si è visto come realizzare una barra
di icone che, più o meno, ha l'aspetto di quelle
presenti in Mac OS X. In realtà, non è
proprio uguale, ma almeno il suo look&feel è
più vicino all'interfaccia Aqua di quanto faccia
l'implementazione Swing di Apple. Una volta in possesso
di questo widget il passo successivo naturale è
quello di integrarlo in una finestra delle preferenze.
Le finestre di preferenze in Mac OS X permettono di
configurare le opzioni del programma. Sono sostanzialmente
l'equivalente delle finestre di dialogo che si ottengono
sotto Windows quando si invoca la funzionalità
Opzioni, solitamente presente sotto il menu Strumenti.
Questo comportamento si ritrova in software quali Internet
Explorer, Outlook o Word.
In Figura 1 si può osservare una finestra delle
preferenze che utilizza la classe IconToolbar sviluppata
nell'articolo precedente. È una finestra di preferenze
per un ipotetico gioco del Sudoku.
Figura 1 - Una finestra delle preferenze di esempio
Come
funziona una finestra di questo tipo? Il meccanismo
è semplice: le icone nella barra in alto permettono
di scegliere l'argomento di configurazione. In funzione
dell'icona attiva viene presentato un determinato contenuto
nell'area della finestra, composto da campi di testo,
checkbox, combo ed altri elementi in funzione delle
configurazioni necessarie. È possibile pensare
alla barra delle icone come pannello a schede avanzato,
dove manca una rappresentazione grafica delle tipiche
linguette che si trovano ad esempio nelle tab di Windows.
Selezionando una icona si ottiene la visualizzazione
della scheda relativa. Se l'area necessaria per una
scheda è maggiore dell'area attuale della finestra,
quest'ultima viene ridimensionata utilizzando una semplice
animazione. In sostanza, si vede la finestra allargarsi
e restringersi per occupare solo lo spazio strettamente
necessario per ospitare i widget contenuti.
Utilizzare
la barra delle icone
Ma prima di passare alla gestione della finestra (che
comunque non vedrà l'animazione del ridimensionamento,
purtroppo), è opportuno affrontare le problematiche
di interfacciamento della barra delle icone con una
finestra Swing. Quello che è necessario implementare,
è sostanzialmente una finestra a schede. Per
fare questo Java offre due meccanismi: la classe JTabbedPane
e la classe CardLayout. Il primo è un widget
Swing che permette di realizzare diversi tipi di schede,
posizionando le linguette in orizzontale o verticale,
in alto, basso, destra o sinistra. Permette di identificare
le schede con una descrizione, una icona o tutti e due.
La classe CardLayout è invece semplicemente un
layout manager che permette di visualizzare alternativamente,
nella stessa area di finestra, diversi contenitori.
La classe JTabbedPane è più completa e
finita, mentre CardLayout richiede codifiche aggiuntive.
Per contro, la prima non permette di una personalizzazione
spinta dell'aspetto grafico. Soppesando pro e contro
di entrambe le classi, si è scelto di utilizzare
CardLayout.
La classe AbstractPreferencesFrame estende JFrame ed
implementa l'infrastruttura necessaria per realizzare
finestre di preferenze. Le sottoclassi dovranno solo
fornire descrizioni ed icone per la barra ed il contenitore
da visualizzare per ciascuna di queste. Il costruttore
si aspetta il titolo della finestra ed il numero dei
pannelli previsti, necessario per inizializzare i vettori
che contengono pulsanti, descrizioni, icone e contenitori:
public
AbstractPreferencesFrame( String title, int panelCount
) {
super(title);
this.panelCount = panelCount;
panels = new JPanel[ panelCount ];
buttons = new JButton[ panelCount ];
descriptions = new String[ panelCount ];
icons = new String[ panelCount ];
setup();
createUI();
}
La
configurazione di questi vettori è demandata
al metodo setup(), che in questa è dichiarato
come astratto: sarà responsabilità delle
sottoclassi implementarlo fornendo il codice di inizializzazione
dei vettori, che saranno utilizzati in createUI() per
inizializzare l'interfaccia utente.
Il metodo createUI() esegue diverse operazioni:
- istanzia
la barra delle icone.
- Crea
un layout manager CardLayout che associa al pannello
dei contenuti.
- Imposta
un bordo vuoto (10,40,20,40) in modo da spaziare il
contenuto della finestra dai bordi, in modo che l'aspetto
sia similare alle finestre Aqua.
- Inserisce
ciascun pannello contenitore (quindi ciascuna "scheda")
nella finestra e lo associa al layout manager. Associare
i pannelli ad entrambi è indispensabile per
ottenere il corretto funzionamento della finestra.
- Imposta
un BorderLayout per la finestra. Mette in alto la
toolbar ed i contenuti al centro. Come si ricorderà,
questo tipo di layout manager lascia al componente
superiore ed inferiore la larghezza desiderata e l'altezza
minima, mentre a quelli laterali lascia l'altezza
desiderata e la larghezza minima; il componente centrale
prende invece lo spazio restante. In questo caso la
toolbar avrà l'altezza minima ma la larghezza
voluta, mentre tutto il resto dello spazio verrà
concesso ai contenitori delle diverse schede. Come
si vedrà in seguito, la dimensione della finestra
verrà comunque manipolata da codice.
- La
finestra viene resa non ridimensionabile tramite il
metodo setResizable().
- La
posizione della finestra viene impostata alle coordinate
50,50 con il metodo setLocation().
- Viene
mostrato il pannello di default tramite il metodo
showPanel(), di cui vedremo in seguito l'implementazione.
Il
codice è il seguente:
protected
void createUI() {
toolbar = createToolbar();
cardLayout = new CardLayout();
contents = new JPanel( cardLayout );
contents.setBorder(
BorderFactory.createEmptyBorder( 10, 40, 20, 40) );
for( int i = 0; i < panelCount; i++ ) {
cardLayout.addLayoutComponent( panels[i], ""+i
);
contents.add( panels[i], ""+i );
}
getContentPane().setLayout(new BorderLayout());
getContentPane().add( toolbar, BorderLayout.NORTH );
getContentPane().add( contents, BorderLayout.CENTER
);
setResizable(false);
setLocation(50,50);
showPanel(defaultPanel);
}
Creazione
della barra delle icone
Il metodo createToolbar() si occupa di istanziare una
IconToolbar utilizzando i vettori configurati dalla
sottoclasse che definisce concretamente la finestra.
Per creare ciascun pulsante viene utilizzato il metodo
createButton(), a cui viene passata la descrizione e
l'icona. Inoltre, a ciascun pulsante è necessario
impostare un ActionListener per gestire l'evento di
clic. L'implementazione è semplice, è
sufficiente infatti invocare il metodo showPanel() passando
l'id del pannello, che non è altro che il progressivo
dello stesso (la numerazione parte da zero):
protected
IconToolbar createToolbar() {
IconToolbar toolbar = new IconToolbar();
for (int i = 0; i < panelCount; i++) {
final int id = i;
buttons[ i ] = toolbar.createButton( descriptions[i],
icons[i]);
buttons[ i ].addActionListener( new ActionListener()
{
public void actionPerformed(ActionEvent e) {
showPanel( id );
}
} );
}
return toolbar;
}
Si
noti che è possibile creare anche un ActionListener
unico per tutti i pulsanti, e capire dall'ActionEvent
passato come parametro al metodo actionPerformed() quale
pulsante è stato selezionato. L'implementazione
qui illustrata è forse quella più "lazy".
Selezione dei pannelli
Ora che la finestra è stata inizializzata correttamente
è necessario passare alla fase di selezione della
scheda attualmente selezionata. Questa funzionalità
è implementata nel metodo showPanel(), che svolge
diverse operazioni:
- invoca
il metodo show() sul layout manager passando il pannello
che contiene tutti i widget ed un id, che è
costruito semplicemente convertendo a stringa l'indice
del pannello passato come parametro. In questo modo
la finestra presenta la scheda selezionata.
- Il
titolo della finestra viene cambiato in modo da riflettere
la descrizione della scheda attualmente visualizzata.
Questa è una caratteristica di Aqua che è
stata replicata. Per impostare il titolo della finestra
viene utilizzato il metodo setTitle().
- Per
tutti i pulsanti della barra delle icone viene impostato
a false il flag contentAreaFilled e borderPainted.
In questo modo il pulsante non disegna né lo
sfondo, né il bordo. L'aspetto risultante è
quello di una icona non selezionata. Si osservi la
Figura 1: questo aspetto è quello delle icone
Nuovo gioco, Temi e Posta Elettronica.
- Una
volta fatto in modo che tutti i pulsanti abbiano l'aspetto
"non selezionato" è necessario fare
in modo che quello effettivamente selezionato sia
invece opportunamente evidenziato. Per fare questo
vengono impostati a true i due flag sopra citati sul
pannello relativo all'indice passato come parametro.
- L'ultima
operazione è relativa al ridimensionamento
della finestra, che si ottiene con il metodo setSize().
Questo si aspetta un oggetto Dimension che viene ottenuto
richiedendo la dimensione preferita al pannello attualmente
visualizzato attraverso il metodo getPreferredSize().
A questo viene aggiunto, in altezza, l'altezza preferita
della toolbar, più un valore fisso di 50 pixel.
Anche
la larghezza viene aggiustata, incrementandola di 80
pixel. Il codice è il seguente:
protected
void showPanel( int panelIndex ) {
cardLayout.show( contents, ""+panelIndex );
setTitle( descriptions[ panelIndex ] );
for (int i=0; i<panelCount; i++) {
buttons[ i ].setContentAreaFilled(false);
buttons[ i ].setBorderPainted(false);
}
buttons[ panelIndex ].setContentAreaFilled(true);
buttons[ panelIndex ].setBorderPainted(true);
Dimension d = panels[ panelIndex ].getPreferredSize();
d.height += toolbar.getPreferredSize().height + 50;
d.width += 80;
setSize(d);
}
Si
potrebbe obiettare che la manipolazione della dimensione
della finestra si basa non solo su calcoli dell'occupazione
dinamica del suo contenuto, ma anche su valori costanti
(50, 80 pixel). In realtà, essendo questo codice
solo un esercizio su Mac OS X, la cosa non è
poi così grave.
Un'altra mancanza di questa implementazione è
che il ridimensionamento, sebbene avvenga correttamente,
non utilizza un'animazione, ma avviene in modo secco.
Purtroppo non sembra esserci un modo semplice per ovviare
a questo problema in maniera elegante, dunque ho duvuto
gettare la spugna.
Creare
una finestra concreta
Come si usa in concreto la classe astratta AbstractPreferencesFrame
per realizzare una finestra delle preferenze? Verrà
ora illustrata PreferencesFrame, la classe concreta
che ha permesso la creazione della Figura 1. Questa
classe definisce 4 pannelli, il cui numero è
inserito in una costante di classe. Il costruttore non
fa altro che richiamare la superclasse, passando il
titolo Preferenze (che verrà comunque sovrascritto
dalla descrizione del pannello di default, che solitamente
è il primo) ed il numero di pannelli. L'implementazione
del metodo setup(), che è stato dichiarato astratto
nella superclasse, non fa altro che valorizzare i diversi
vettori che ospitano descrizioni, nomi di icone e pannelli
delle singole schede. Si noti la terza descrizione.
Viene utilizzato un trucchetto per spaziare il pulsante:
sono sati aggiunti quattro spazi prima e dopo il testo,
per rendere la stringa più lunga:
public
class PreferencesFrame extends AbstractPreferencesFrame
{
private static final int PANEL_COUNT = 4;
public PreferencesFrame() {
super("Preferenze", PANEL_COUNT);
}
protected void setup() {
descriptions[0] = "Generale";
descriptions[1] = "Nuovo gioco";
descriptions[2] = " Temi ";
descriptions[3] = "Posta elettronica";
icons[0] = "GeneralPref.png";
icons[1] = "NewGamePref.png";
icons[2] = "IconPref.png";
icons[3] = "MailPref.png";
panels[0] = createGeneralePanel();
panels[1] = createNuovoGiocoPanel();
panels[2] = createTemiPanel();
panels[3] = createMailPanel();
}
Creare
un pannello
Ovviamente è necessario implementare tutti i
metodi createXXXPanel() sopra utilizzati, per fare in
modo che ciascuna icona abbia il proprio pannello, con
i relativi componenti visuali. L'implementazione di
questi pannelli utilizza le normali API Swing. Ad esempio,
il pannello di creazione dei dati di posta elettronica
(Figura 2), è ottenuto con il codice seguente:
Figura 2 - estremi per l'invio via mail di un
gioco
private
JPanel createMailPanel() {
JPanel p = new JPanel(new BorderLayout());
GridLayout gridLayout = new GridLayout(3,2);
gridLayout.setVgap(10);
JPanel p1 = new JPanel(gridLayout);
JTextField nomeTextField = new JTextField(10);
nomeTextField.setText( System.getProperty("user.name")
);
p1.add( createRightOrientedLabel(
"Indirizzo di posta del mittente: ") );
p1.add( new JTextField(10) );
p1.add( createRightOrientedLabel("Nome mittente:
") );
p1.add( nomeTextField );
p1.add( createRightOrientedLabel("Indirizzo server
SMTP: ") );
p1.add( new JTextField(15) );
p.add( p1, BorderLayout.NORTH );
return p;
}
In questo caso il layout manager della finestra
è un GridLayout di tre righe per due colonne.
Ciascuna cella contiene un elemento: nella colonna di
sinistra si trovano le descrizioni ed a destra i campi
di testo. Si noti che GridLayout distribuisce tutto
lo spazio uniformemente a tutte le celle, indipendentemente
dal contenuto. Se la finestra fosse più alta,
infatti, troveremmo dei campi di testo altissimi, che
sono brutti da vedere ed inutili. Per evitare questo,
il pannello con layout manager GridLayout viene inserito
in un ulteriore pannello, con layout manager BorderLayout,
nella posizione NORTH. In questo modo l'altezza di tutto
il pannello con GridLayout sarà solo quella minima
indispensabile, nonostante l'altezza della finestra.
Un altro particolare da notare è l'uso del metodo
setVgap() che permette di impostare su GridLayout la
spaziatura verticale in pixel tra una riga e l'altra.
Conclusioni
In
questo numero abbiamo visto come costruire una finestra
delle preferenze in stile Aqua, utilizzando diversi
particolari delle API Swing. Nel prossimo numero vedremo
come eseguire la persistenza delle opzioni di configurazione
in modo semplice e riutilizzabile.
|