In un'interfaccia a riga di comando il codice decide la sequenza: chiede un dato con Scanner, stampa con System.out, cicla e prosegue quando vuole lui. In un'interfaccia grafica (GUI) l'utente interagisce con componenti grafici, quindi il programma non sa in anticipo che cosa succederà. Costruisce l'interfaccia, poi attende e reagisce.
Questa è la programmazione guidata da eventi (event driven programming). Un evento è un'azione a cui il programma può reagire: la pressione di un tasto, un clic, un gesto sullo schermo di un telefono. Il programma non chiede più l'input, ma prepara le reazioni agli eventi che possono arrivare.
I componenti di Swing
Una libreria grafica offre componenti già pronti, detti widget: etichette, pulsanti, pannelli, menu, aree di testo. Non si disegna da zero l'aspetto di un pulsante, e lo stesso codice può adottare l'aspetto del sistema ospitante configurando il look and feel.
Swing fa parte delle Java Foundation Classes (JFC) ed estende l'Abstract Window Toolkit (AWT), che sta più vicino al disegno di basso livello. I pacchetti principali sono javax.swing.* e javax.swing.event.
La gerarchia dei contenitori
Ogni elemento grafico è un Component. Un Container è un Component che può contenerne altri. Sotto di lui ci sono Window, la finestra, e Panel. La finestra ha due sottoclassi: Frame, la finestra dell'applicazione, e Dialog, la finestra di dialogo. Le classi Swing sono JFrame e JDialog.
Gerarchia di base dei contenitori
Ogni finestra è un contenitore, e ogni contenitore è un componente. JFrame e JDialog sono le versioni Swing di Frame e Dialog.
Una finestra di dialogo può essere modale: impedisce di usare le altre finestre finché resta aperta, come JOptionPane. Un JDialog può anche non essere modale.
Un'applicazione a finestre ha almeno un contenitore di livello superiore (top level container): JFrame o JDialog. È la radice di una gerarchia di contenimento, cioè un albero in cui ogni contenitore include pannelli e altri elementi.
Ogni contenitore di livello superiore ha un content pane: l'area interna in cui si aggiungono i componenti. Il JFrame è come una cornice, con bordo e barra del titolo, e il content pane è l'interno. Il JRootPane dentro la finestra ha anche un layered pane, che organizza content pane e barra dei menu, e un glass pane davanti a tutto. Qui si usa solo il content pane.
La prima finestra
import javax.swing.*;public class GUIApp { public static void main(String[] args) { JFrame frm = new JFrame("Hello World!"); frm.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); frm.setVisible(true); System.out.println("Ciao ciao"); }}
Una finestra appena creata è invisibile: setVisible(true) la mostra. Decidere quando mostrarla fa parte delle scelte di esperienza d'uso, perché il programma può creare finestre e farle vedere più tardi.
main finisce subito, ma il programma no. Il meccanismo che gestisce gli eventi resta attivo e attende l'utente finché la finestra è aperta. Per questo, lanciato da terminale, il programma stampa «Ciao ciao» e non restituisce il prompt.
Si configura prima di mostrare. Se la finestra è già visibile, l'utente può agire prima che il suo comportamento sia definito. Per esempio, può premere la chiusura prima che EXIT_ON_CLOSE sia impostato. L'esito dipende allora dall'ordine delle operazioni: è un caso di race condition.
Che cosa fa la chiusura
setDefaultCloseOperation stabilisce che cosa accade quando l'utente preme il pulsante di chiusura.
DO_NOTHING_ON_CLOSE: non parte nessuna azione automatica. Serve quando l'interfaccia deve restare sempre attiva, come in un dispositivo embedded.
HIDE_ON_CLOSE: nasconde la finestra, che resta in memoria e si può riaprire con setVisible(true). È il comportamento predefinito di JFrame.
DISPOSE_ON_CLOSE: elimina la finestra e libera le risorse grafiche.
EXIT_ON_CLOSE: termina il programma, come System.exit(0).
Qui l'applicazione coincide con la finestra principale, quindi si usa EXIT_ON_CLOSE.
Metodi di JFrame
JFrame() crea una finestra senza titolo; JFrame(String) imposta il titolo, e setTitle lo cambia.
setSize(larghezza, altezza) dà la dimensione in pixel; pack() la calcola dalle dimensioni preferite dei componenti.
setLocation(x, y) posiziona l'angolo in alto a sinistra della finestra.
getContentPane() e setContentPane(...) leggono e sostituiscono il content pane; setJMenuBar(...) imposta la barra dei menu.
Le coordinate hanno origine nell'angolo in alto a sinistra: x cresce verso destra e y verso il basso. Su uno schermo da 1920×1080 pixel, x va da 0 a 1919 e y da 0 a 1079.
Aggiungere componenti
I componenti si aggiungono al content pane, che si ottiene con getContentPane(). Si usa add, metodo di Container; la classe Container sta in java.awt, quindi si importa java.awt.*. La finestra si mostra per ultima, così l'utente la vede già costruita e non la vede riempirsi pezzo per pezzo.
import javax.swing.*;import java.awt.*;public class GUIApp { public static void main(String[] args) { JFrame frm = new JFrame("Prima finestra"); frm.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); frm.setSize(200, 200); Container frmContentPane = frm.getContentPane(); frmContentPane.add( new JLabel("Buona Lezione")); frm.setVisible(true); }}
Senza setSize la finestra ha la dimensione predefinita e può non mostrare tutto il contenuto, titolo compreso.
Una classe per finestra
Con molte finestre il main diventa troppo grande e difficile da mantenere. Si crea allora una classe per ogni finestra, che estende JFrame e costruisce da sola la propria interfaccia. Il resto del programma non ne conosce i dettagli: è incapsulamento e occultamento dell'informazione. La classe è final, così nessuno la estende e ne cambia il comportamento.
import javax.swing.*;public final class MyFrame extends JFrame { private static final String titolo = "Prima finestra"; private static final JLabel testo = new JLabel("Buona Lezione"); private static final int larghezza = 200, altezza = 200; public MyFrame() { super(titolo); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); this.setSize(larghezza, altezza); this.getContentPane().add(testo); this.setVisible(true); } public static void main(String[] args) { new MyFrame(); }}
Il main si limita a creare la finestra, perché tutta la logica sta nella classe.
Model View Controller
Un programma con finestre si divide in tre logiche. È un'introduzione semplificata al pattern Model View Controller (MVC).
Logica
Contenuto
Presentazione
classi delle finestre, che gestiscono l'interazione grafica
Gestione delle informazioni
tipi di dato astratti che rappresentano i dati
Applicativa
coordina la grafica con la gestione delle informazioni
Layout manager
Un layout manager è un algoritmo che decide come disporre i componenti nel content pane. Si imposta con setLayout. Negli esempi che seguono il main, che si limita a eseguire new MyFrame();, è omesso. Un layout serve in un solo punto, quindi si crea direttamente come argomento: setLayout(new FlowLayout()). Il layout predefinito del content pane è BorderLayout.
FlowLayout
Dispone i componenti riga per riga, da sinistra a destra, finché c'è spazio. Il primo che non entra va nella riga successiva, e ogni riga è centrata. Il costruttore new FlowLayout(allineamento) sceglie l'allineamento: FlowLayout.LEADING a sinistra, FlowLayout.CENTER al centro, FlowLayout.TRAILING a destra.
import javax.swing.*;import java.awt.*;public final class MyFrame extends JFrame { 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++) { frmContentPane.add( new JButton(String.valueOf(i))); } this.setVisible(true); }}
Il numero di righe dipende dalla larghezza della finestra: cambiando il valore di setSize cambia la disposizione. Il ciclo evita di dichiarare ogni pulsante a mano quando i componenti aumentano.
FlowLayout al variare della larghezza
12345
finestra
Allargando la finestra più pulsanti entrano in una riga; quando manca spazio il successivo va a capo. Ogni riga è centrata.
GridLayout
Dispone i componenti in una griglia di celle tutte uguali, riga per riga, da sinistra a destra. Ogni componente riempie la propria cella. Il costruttore riceve righe e colonne. Il gioco del quindici ha quindici tessere numerate da 0 a 14 in una griglia 4×4, con una cella libera.
import javax.swing.*;import java.awt.*;public final class MyFrame extends JFrame { public MyFrame() { super("GridLayout e JButton"); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); this.setSize(200, 200); Container frmContentPane = this.getContentPane(); frmContentPane.setLayout( new GridLayout(4, 4)); for (int i = 0; i < 15; i++) { frmContentPane.add( new JButton(String.valueOf(i))); } this.setVisible(true); }}
Il ciclo si ferma a 14, quindi crea quindici pulsanti e la sedicesima cella resta vuota. String.valueOf(i) dà l'etichetta, perché il costruttore di JButton vuole una stringa.
BorderLayout
Divide il contenitore in cinque regioni: PAGE_START (o NORTH), PAGE_END (o SOUTH), LINE_START (o WEST), LINE_END (o EAST) e CENTER. Si aggiunge un componente indicando la regione. Una regione senza componente non compare e la finestra si adatta.
Le cinque regioni di BorderLayout
PAGE_STARTPAGE_ENDLINE_STARTLINE_ENDCENTER
content pane
Il contenitore è diviso in cinque regioni: ogni componente va in quella indicata.
Il content pane usa già BorderLayout, quindi qui non serve setLayout.
import javax.swing.*;import java.awt.*;public final class MyFrame extends JFrame { public MyFrame() { super("BorderLayout e JButton"); this.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE); this.setSize(300, 200); Container frmContentPane = this.getContentPane(); frmContentPane.add( new JButton("North"), BorderLayout.PAGE_START); frmContentPane.add( new JButton("South"), BorderLayout.PAGE_END); frmContentPane.add( new JButton("West"), BorderLayout.LINE_START); frmContentPane.add( new JButton("East"), BorderLayout.LINE_END); frmContentPane.add( new JButton("Center"), BorderLayout.CENTER); this.setVisible(true); }}