Un'interfaccia con molti widget si progetta come gerarchia di contenimento: un componente ne contiene altri, fino ai nodi foglia, che non contengono nulla. Il contenitore più usato è JPanel, che ospita altri pannelli e widget. Il content pane di un JFrame usa BorderLayout, mentre un JPanel appena creato usa FlowLayout: al pannello si assegna quindi esplicitamente il layout voluto.
Si progetta dall'alto: il layout del content pane, poi per ogni area un JPanel con il suo layout, fino ai widget semplici come JButton e JLabel. Il risultato è l'albero della GUI, annotato con i layout.
Una finestra con opzioni
La finestra ha un'etichetta in alto, due caselle JCheckBox al centro e i pulsanti OK e Annulla in basso: tre aree, quindi il content pane ha BorderLayout. Le caselle stanno una sopra l'altra con GridLayout(2, 1); etichetta e pulsanti usano FlowLayout. I widget hanno nomi parlanti, perché serviranno nel programma.
L'albero della finestra
Ogni componente contiene quelli collegati alla sua destra. Si progetta da sinistra a destra, ma il codice crea prima i componenti più a destra.
Costruire dal basso
Un oggetto si aggiunge a un contenitore solo se esiste già. Il codice segue quindi l'albero al contrario: si creano le foglie, poi i pannelli che le contengono, infine si aggiungono i pannelli al content pane.
import javax.swing.*;import java.awt.*;public final class MyFrame extends JFrame { private static final String titolo = "Contenitori Multipli"; // pannello nord private static final JLabel infoLbl = new JLabel("Selezionare"); private static final JPanel nordPnl = new JPanel(); // pannello centrale private static final JCheckBox opz1Chk = new JCheckBox("Opz1"); private static final JCheckBox opz2Chk = new JCheckBox("Opz2"); private static final JPanel centroPnl = new JPanel(new GridLayout(2, 1)); // pannello sud private static final JButton okBtn = new JButton("OK"); private static final JButton cancBtn = new JButton("Annulla"); private static final JPanel sudPnl = new JPanel(); public MyFrame() { super(titolo); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); // dalle foglie ai pannelli nordPnl.add(infoLbl); centroPnl.add(opz1Chk); centroPnl.add(opz2Chk); sudPnl.add(okBtn); sudPnl.add(cancBtn); // dai pannelli al content pane Container frmContentPane = this.getContentPane(); frmContentPane.add( nordPnl, BorderLayout.NORTH); frmContentPane.add( centroPnl, BorderLayout.CENTER); frmContentPane.add( sudPnl, BorderLayout.SOUTH); this.pack(); this.setLocationRelativeTo(null); this.setVisible(true); }}
La chiusura si imposta per prima. pack() dimensiona la finestra sui componenti, setLocationRelativeTo(null) la centra e setVisible(true) la mostra per ultima. La finestra non reagisce ancora, perché non ha ascoltatori.
Componenti comuni
JButton; JCheckBox (scelte indipendenti); JRadioButton (una sola scelta).
JComboBox (elenco a discesa); JList (anche a selezione multipla); JSpinner; JSlider.
Testo: JTextField; JFormattedTextField; JPasswordField (nasconde i caratteri); JTextArea; JEditorPane (testo formattato, per esempio HTML); JTextPane (stili e contenuti incorporati come le immagini).
I menu
Un menu organizza le funzioni in modo gerarchico; su desktop sta in una barra in alto.
Un JFrame ha al più una JMenuBar, assegnata con setJMenuBar. Non va nel content pane, che è la parte disegnabile sotto di essa.
La barra contiene i JMenu, che si creano con il loro nome e si inseriscono con add.
Un JMenu contiene voci JMenuItem e altri JMenu, i sottomenu; un JSeparator raggruppa le voci con una linea.
Anche i menu formano un albero: si progetta dalla barra e si costruisce dalle voci.
L'albero dei menu
La barra contiene i menu, un menu contiene voci e sottomenu. Si costruiscono prima le voci e per ultima la barra.
import javax.swing.*;public class MyMenu extends JFrame { // menu Move private static final JMenuItem upIt = new JMenuItem("Up"); private static final JMenuItem dwnIt = new JMenuItem("Down"); private static final JMenu moveMenu = new JMenu("Move"); // menu Edit private static final JMenuItem rndmIt = new JMenuItem("Random"); private static final JMenu editMenu = new JMenu("Edit"); // menu File private static final JMenuItem newIt = new JMenuItem("New"); private static final JMenuItem openIt = new JMenuItem("Open"); private static final JMenuItem saveIt = new JMenuItem("Save as..."); private static final JMenu fileMenu = new JMenu("File"); private final JMenuBar menuBar = new JMenuBar(); public MyMenu() { super("Un Menu di Esempio"); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); moveMenu.add(upIt); moveMenu.add(dwnIt); editMenu.add(moveMenu); editMenu.add(rndmIt); fileMenu.add(newIt); fileMenu.add(openIt); fileMenu.add(saveIt); menuBar.add(fileMenu); menuBar.add(editMenu); this.setJMenuBar(menuBar); this.pack(); this.setLocationRelativeTo(null); this.setVisible(true); }}
Il mock-up
L'interazione con l'utente si disegna in un mock-up: una bozza dell'interfaccia, a mano o con uno strumento grafico. L'utente descrive male a parole ciò che vuole, ma reagisce a un disegno, quindi il mock-up fa emergere i requisiti. Le strutture dati si modellano invece in UML. L'ordine è: mock-up, albero della GUI, implementazione.
Programmazione a eventi
In un'applicazione reattiva il programma non decide la sequenza: attende e risponde a ciò che l'utente o l'ambiente producono, come tasti, tocchi, messaggi di rete e timer. Su una riga di comando era l'utente ad aspettare il programma.
Un sistema a eventi ha tre elementi:
la sorgente: l'oggetto che genera l'evento;
l'evento: l'oggetto che descrive che cosa è successo;
l'ascoltatore (listener): l'oggetto con la logica per reagire.
Nella event delegation la sorgente notifica l'evento agli ascoltatori registrati e delega loro la gestione. Una sorgente può avere più ascoltatori e uno stesso ascoltatore può servire più sorgenti. «Evento» indica sia il tipo, come la pressione di un pulsante, sia l'oggetto creato per una singola pressione.
Registrare un ascoltatore
Si identifica il tipo di evento: a XxxEvent corrisponde l'interfaccia XxxListener.
Si scrive una classe che implementa l'interfaccia: i suoi metodi sono i gestori dell'evento.
Si registra un'istanza presso la sorgente con addXxxListener.
Se la lista degli ascoltatori di una sorgente è vuota non succede nulla e non c'è alcun errore: per questo le finestre disegnate finora non reagivano. Creare l'ascoltatore non basta: finché non è registrato non riceve eventi. Si registra nel costruttore, prima di setVisible(true), perché a finestra visibile l'utente potrebbe agire prima che l'ascoltatore sia pronto.
A runtime l'utente agisce sulla sorgente e la sorgente passa l'oggetto evento agli ascoltatori registrati. Il metodo gestore lo riceve già creato: il programma non lo crea mai con new. Il gestore parte per iniziativa del framework, non per una chiamata nel main: non si sa che cosa ci fosse prima sullo stack né quando arriverà l'evento, quindi si possono fare assunzioni solo sullo stato locale.
Eventi del mouse
Un MouseEvent nasce quando il puntatore entra o esce dall'area di un componente, o quando un pulsante viene premuto, rilasciato o cliccato (premuto e rilasciato). L'evento fornisce getX() e getY(), le coordinate relative al componente, getButton(), che dà MouseEvent.NOBUTTON, BUTTON1, BUTTON2 o BUTTON3, e getClickCount(), il numero di clic.
MouseListener dichiara cinque metodi: mouseClicked, mouseEntered, mouseExited, mousePressed e mouseReleased. Per un evento che non interessa il corpo è vuoto, ma il metodo va scritto, perché lo richiede l'interfaccia.
import java.awt.event.*;public class MouseSpy implements MouseListener { public void mouseClicked(MouseEvent e) { System.out.println("Click su (" + e.getX() + "," + e.getY() + ")"); } public void mousePressed(MouseEvent e) { System.out.println("Premuto su (" + e.getX() + "," + e.getY() + ")"); } public void mouseReleased(MouseEvent e) { System.out.println("Rilasciato su (" + e.getX() + "," + e.getY() + ")"); } public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {}}
import javax.swing.*;public class MyFrame extends JFrame { public MyFrame() { super("MouseTest"); // registra l'ascoltatore presso il frame this.addMouseListener(new MouseSpy()); this.setSize(200, 200); this.setVisible(true); } public static void main(String[] args) { new MyFrame(); }}
Gli eventi arrivano solo dal componente registrato. Questa finestra non imposta la chiusura: la X la nasconde e il programma continua a girare. Un'applicazione robusta ha un comportamento determinato anche per azioni non previste, quindi definisce la chiusura e le altre azioni sulla finestra.
Altri eventi e ascoltatori
Evento
Ascoltatore
Rileva
FocusEvent
FocusListener
focus da tastiera ottenuto o perso
KeyEvent
KeyListener
tasto premuto, rilasciato, battuto
MouseEvent
MouseMotionListener
puntatore spostato o trascinato
WindowEvent
WindowListener
apertura, chiusura, icona, attivazione (7 metodi)
ActionEvent e un ascoltatore per più sorgenti
Un ActionEvent rappresenta un'azione dell'utente: il clic su un pulsante, la scelta di una voce di menu, Invio in un campo di testo, la selezione in una casella combinata o in un pulsante radio. ActionListener ha il solo metodo actionPerformed(ActionEvent e).
Cinque pulsanti devono mostrare una finestra che dice quale è stato premuto, e basta un solo ascoltatore. getSource() restituisce la sorgente come Object: è un pulsante, quindi si converte in JButton e getText() ne dà l'etichetta.
import java.awt.event.*;import javax.swing.*;public class MyFrameListener implements ActionListener { public void actionPerformed(ActionEvent e) { JButton b = (JButton) e.getSource(); JOptionPane.showMessageDialog(null, "Hai premuto il pulsante: " + b.getText()); }}
La finestra crea l'ascoltatore una volta e lo registra su ogni pulsante:
import javax.swing.*;import java.awt.*;public final class MyFrame extends JFrame { private static final MyFrameListener listener = new MyFrameListener(); public MyFrame() { super("FlowLayout e JButton"); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); this.setSize(300, 100); Container frmContentPane = this.getContentPane(); frmContentPane.setLayout(new FlowLayout()); for (int i = 1; i <= 5; i++) { JButton b = new JButton(String.valueOf(i)); b.addActionListener(listener); frmContentPane.add(b); } this.setVisible(true); }}
Il campo listener è solo un riferimento: il collegamento nasce quando si chiama addActionListener sul pulsante.
Leggere un altro componente dall'ascoltatore
Una finestra ha un campo di testo e un pulsante. Alla pressione del pulsante una finestra informativa deve mostrare il testo scritto.
import javax.swing.*;import java.awt.*;public class EsempioTextField extends JFrame { private static final JPanel centro = new JPanel(); private static final JPanel sud = new JPanel(); private static final JTextField campoTesto = new JTextField(20); private static final JButton button = new JButton("Premi"); public EsempioTextField() { super("Esempio"); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); centro.add(campoTesto); sud.add(button); this.getContentPane().add( centro, BorderLayout.CENTER); this.getContentPane().add( sud, BorderLayout.SOUTH); // qui si registra l'ascoltatore this.pack(); this.setVisible(true); }}
getSource() dà solo il pulsante, mentre il testo sta in un altro componente. Gli schemi per raggiungerlo sono quattro, e nessuno è sempre preferibile.
Classe interna. Definita dentro la finestra, legge anche i campi privati. Va bene solo per ascoltatori semplici e locali: in generale è da evitare, ma il codice generato automaticamente può usarla.
import java.awt.event.*;import javax.swing.*;public class EsempioTextField extends JFrame { public EsempioTextField() { // ... button.addActionListener( new EsempioTextFieldListener()); // ... } private class EsempioTextFieldListener implements ActionListener { public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog( null, campoTesto.getText()); } }}
La finestra è l'ascoltatore. La classe che estende JFrame implementa anche ActionListener e registra this: l'oggetto ha due ruoli e actionPerformed vede i suoi campi. È poco modulare.
import java.awt.event.*;import javax.swing.*;public class EsempioTextField extends JFrame implements ActionListener { public EsempioTextField() { // ... button.addActionListener(this); // ... } public void actionPerformed(ActionEvent e) { JOptionPane.showMessageDialog( null, campoTesto.getText()); }}
Classe esterna. Riceve la finestra nel costruttore e da fuori vede solo ciò che è pubblico, quindi la finestra espone un getter. La finestra crea l'ascoltatore passandogli this e lo registra.
// nella finestra EsempioTextFieldpublic EsempioTextField() { // ... button.addActionListener( new EsempioTextFieldListener(this)); // ...}public JTextField getTextField() { return campoTesto;}
Package dedicato. Ciò che non ha modificatore di accesso è visibile alle classi dello stesso package. Finestra e ascoltatori stanno nello stesso package, dichiarato in testa a ogni file: se i campi della finestra non hanno modificatore, gli ascoltatori li leggono senza getter.