MokaByte 101 - 9mbre 2005
 
MokaByte 101 - 9mbre 2005 Prima pagina Cerca Home Page

 

 

 

Applicazioni Desktop
Le finestre delle preferenze

Proseguiamo la serie di articoli sulla realizzazioni di applicazioni desktop illustrando come utilizzare la toolbar Aqua sviluppata nel precedente numero per realizzare una finestra delle preferenze in stile Mac OS X. Ancora una volta, l'obiettivo non è emulare l'interfaccia Apple, ma imparare qualcosa di nuovo su Java e Swing

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.