Metodologie di Programmazione: Lezione 17

Riccardo Silvestri

Hello JavaFX

La prima libreria di Java per lo sviluppo di interfacce grafiche, graphic user interfaces, in breve GUI, è AWT (Abstract Window Toolkit) introdotta nel 1996. È semplicemente un'astrazione della sottostante GUI nativa. Quindi AWT può gestire solamente le componenti e le loro caratteristiche che sono comuni a tutte le piattaforme sulle quali la JVM è supportata. Questo significa che la libreria AWT è necessariamente piuttosto povera. L'unico vantaggio è che l'aspetto delle componenti della GUI è esattamente quello delle componenti native della piattaforma sottostante. Ma con AWT è praticamente impossibile modificare un componente o crearne di nuovi.

Più o meno nello stesso periodo in cui fu introdotta AWT, Netscape sviluppò la libreria Internet Foundation Classes, IFC. A differenza di AWT, IFC non è basata su wrapper intorno alle componenti native ma su componenti che sono gestite e rese graficamente in modo del tutto indipendente dalla piattaforma sottostante. Nel 1997 la Sun Microsystems (l'allora proprietaria di Java) annunciò l'intenzione di fondere IFC in Java. Il framework Java Foundation Classes, JFC, è il risultato della fusione che comprende AWT, Java2D e Swing. Il framework è stato parte di Java SE fin dal 1998. Swing è stato da allora e fino a Java 7 la principale libreria per lo sviluppo di GUI in Java. La libreria Swing insieme a Java2D, essendo libera da vincoli dovuti alla sottostante piattaforma, ha enormemente arricchito l'arsenale delle componenti e degli strumenti per la costruzione di GUI. Però dal 1998 ad oggi le GUI si sono evolute e Swing ha iniziato a manifestare alcune delle sue debolezze. Molti degli effetti (ad es. riflessioni o effetti blur) che sono divenuti comuni nelle moderne applicazioni sono o impossibili con Swing o molto difficili da realizzare. Lo stesso dicasi per le animazioni. Inoltre dare speciali aspetti alle componenti o crearne di nuove, anche se possibile in Swing, risulta essere un compito molto arduo. Queste sono le principali ragioni che hanno portato alla sostituzione di Swing.

Nel 2007 Sun Microsystems introdusse una tecnologia chiamata JavaFX con lo scopo di competere con l'allora molto in voga Flash. Però all'inizio JavaFX anche se era eseguito sulla JVM aveva un suo linguaggio di programmazione chiamato JavaFX script. Da allora si sono succedute diverse versioni di JavaFX fino all'attuale JavaFX 8 che usa il linguaggio Java ed è distribuita sia nel JRE che nel JDK. Rispetto a Swing JavaFX offre una maggiore varietà di componenti, supporto per grafica 3D, strumenti per facilitare l'uso di effetti ed animazioni. Oltre a ciò JavaFX offre i tipici benefici dello sviluppo di interfacce tramite HTML/CSS grazie a FXML che è un linguaggio di markup basato su XML che permette di definire, in modo dichiarativo, l'intero layout di un'applicazione JavaFX.

In questo prima lezione dedicata a JavaFX diamo le basi per lo sviluppo di applicazioni JavaFX e introduciamo alcuni meccanismi come la gestione di eventi e componenti fondamentali come finestre, controlli e layout.

Le basi di JavaFX

Introduciamo un nuovo package nella nostra code base mp.gui per sperimentare con GUI e in particolare con JavaFX.

Per realizzare un'applicazione JavaFX è necessario estendere la classe astratta Application nel package javafx.application. Questa contiene un solo metodo astratto abstract void start(Stage primaryStage) che ovviamente deve essere implementato, vedremo fra poco il significato del parametro primaryStage. Ma vediamo subito la più semplice applicazione JavaFX che si possa scrivere1, non fa molto, mostra solamente una finestra

package mp.gui;

import javafx.application.Application;
import javafx.stage.Stage;

/** Una classe per fare le prime sperimentazioni con JavaFX */
public class TestGUI extends Application {
    public static void main(String[] args) {
        launch(args);  // Lancia l'applicazione, ritorna quando l'applicazione
    }                  // termina. Può essere invocato una sola volta

    @Override
    public void start(Stage primaryStage) {
        primaryStage.show();  // Rende visibile la finestra (o stage) principale
    }
}

Lo stile della finestra dipende dalla piattaforma (la figura è relativa a MacOS X). Si può notare che la finestra è perfettamente funzionante, trascinabile e ridimensionabile.

Il metodo statico void launch(String... args) di Application, lancia l'applicazione JavaFX. Più precisamente

  1. Costruisce un'istanza della classe che estende Application, nel nostro caso TestGUI.
  2. Invoca il metodo void init() dell'istanza che, come si intuisce dal nome, può essere ridefinito per inizializzare risorse o altro. L'implementazione di default non fa nulla.
  3. Invoca il metodo start(javafx.stage.Stage), sempre dell'istanza, passandogli un'istanza della classe Stage del package javafx.stage. Un oggetto Stage rappresenta una finestra di primo livello (top level window), cioè non è contenuta in altri componenti. Quella che è passata a start è costruita dal metodo launch ed è la finestra principale (primaryStage), l'applicazione può crearne altre.
  4. Aspetta che l'applicazione termini. L'applicazione termina quando o è invocato il metodo Platform.exit() o l'ultima finestra viene chiusa.
  5. Invoca il metodo void stop() dell'istanza dell'applicazione che può essere ridefinito per rilasciare risorse e cose simili. L'implementazione di default non fa nulla.

Il metodo launch crea anche uno speciale thread, chiamato JavaFX Application Thread, in cui è invocato il metodo start, sono elaborati tutti gli eventi di input e sono eseguite le animazioni. La creazione e la modifica di tutti gli oggetti che fanno parte della GUI di JavaFX deve essere fatta nel JavaFX Application Thread. Il metodo init invece è invocato nel thread in cui è stato invocato launch quindi in init non si devono creare o modificare oggetti della GUI. Invece il metodo stop è invocato nel JavaFX Application Thread.

Una finestra Stage di JavaFX è il contenitore di primo livello. Tutti gli altri componenti, controlli, immagini, grafica, ecc. sono contenuti in uno Stage. Però lo Stage può contenere direttamente solamente un oggetto di tipo Scene, nel package javafx.scene. Uno Scene contiene un cosiddetto grafo di scena ( scene graph) che è una struttura ad albero i cui nodi sono i componenti di una GUI. Un qualsiasi componente della GUI, bottone, testo, grafico, immagine, ecc. è rappresentata tramite un sotto-tipo del tipo astratto Node, sempre in javafx.scene. I Node possono essere di due tipologie: quelli che possono avere figli, cioè possono contenere altri nodi, e quelli che non possono avere figli e sono le foglie di uno scene graph. I nodi che possono contenere altri nodi sono sotto-tipi del tipo astratto Parent che è ovviamente un sotto-tipo di Node. Uno Scene contiene solamente il Node che è la radice del grafo di scena. Generalmente la radice è un sotto-tipo di Parent, altrimenti non potrebbe contenere altri nodi. Come vedremo ci sono molti tipi diversi di nodi contenitori, ad esempio i layout che permettono di dislocare controlli con diverse organizzazioni. Un nodo contenitore di base è Group, nel package javafx.scene. Questo è usato sopratutto per contenere nodi di grafica perché i nodi devono essere posizionati specificando esplicitamente le loro coordinate.

Forme

Vediamo un semplice esempio

public void start(Stage primaryStage) {
    Rectangle r = new Rectangle(40, 20, 200, 80);  // Rettangolo in (40, 20)
    Ellipse e = new Ellipse(120, 70, 80, 60); // Ellisse con centro (120, 70)
    e.setFill(Color.rgb(255,0,0));
    Group root = new Group(r, e);       
    Scene scene = new Scene(root, 400, 200);  // La radice del grafo di scena
    scene.setFill(Color.AQUA);            // Colore di background della scena
    primaryStage.setScene(scene);         // La scena della finestra
    primaryStage.show();    // Rende visibile la finestra (o stage) principale
}

Il package javafx.scene.shape contiene classi (sotto-tipi di Node) che rappresentano molte forme 2D come ad esempio quelle che abbiamo usato nell'esempio Rectangle e Ellipse.

Volendo fare diverse prove usando sempre la stessa classe TestGUI, ci conviene introdurre un metodo che crea il grafo di scena e ne ritorna la radice

public void start(Stage primaryStage) {
    Parent root = createShapes();         // La radice del grafo di scena
    Scene scene = new Scene(root, 400, 200);
    scene.setFill(Color.AQUA);            // Colore di background della scena
    primaryStage.setScene(scene);         // La scena della finestra
    primaryStage.show();    // Rende visibile la finestra (o stage) principale
}

private Parent createShapes() {
    Rectangle r = new Rectangle(40, 20, 200, 80);  // Rettangolo in (40, 20)
    Ellipse e = new Ellipse(120, 70, 80, 60); // Ellisse con centro (120, 70)
    e.setFill(Color.rgb(255,0,0));
    return new Group(r, e);
}

Testo e layout

I nodi contenitori più usati sono quelli che permettono di disporre i nodi in modo automatico secondo schemi prefissati, ad esempio incolonnati, su una riga o in una griglia, ecc. Questi sono detti layout e si trovano nel package javafx.scene.layout. Vediamo un esempio con il layout VBox che dispone i nodi in verticale.

private Parent createTexts() {
    Text t1 = new Text("Hello JavaFX");
    Text t2 = new Text("Ciao");
    VBox vb = new VBox(t1, t2);
    vb.setAlignment(Pos.CENTER);  // Allineamento dei nodi
    vb.setSpacing(30);            // Spazio tra i nodi
    return vb;
}

Ovviamente in start sostituiamo la linea Parent root = createShapes(); con Parent root = createTexts();.

Abbiamo usato nodi che rappresentano testo Text nel package javafx.scene.text. Si noti che i layout a differenza di Group si ridimensionano automaticamente quando le dimensioni della finestra cambiano.

Controlli ed eventi

Vediamo ora un esempio con un controllo, un bottone che quando cliccato mostra o nasconde un testo.

private Parent createTextChange() {
    Text txt = new Text("Hello JavaFX");
    Button showHide = new Button("Show/Hide");  
    VBox vb = new VBox(txt, showHide);
    vb.setAlignment(Pos.CENTER);  // Allineamento dei nodi
    vb.setSpacing(30);            // Spazio tra i nodi
    return vb;
} 

Il bottone che abbiamo creato Button è nel package javafx.scene.control che contiene tutti controlli di JavaFX. Per ora cliccando sul bottone non accade nulla perché non abbiamo specificato l'azione da compiere a seguito dell'evento di click. Per specificare un'azione possiamo usare il metodo di Button void setOnAction(EventHandler<ActionEvent> act) che imposta l'azione act che deve essere eseguita quando il bottone è fired, il che accade quando è cliccato, è toccato (su una device con touching screen), tramite un tasto premuto o con l'invocazione diretta del metodo fire(). L'azione è rappresentata dall'interfaccia funzionale EventHandler<T extends Event> nel package javafx.event. L'unico metodo dell'interfaccia è void handle(T event) che è appunto invocato su un certo EventHandler quando si verifica un evento di tipo T relativamente a un nodo per cui quel EventHandler è stato impostato. Nel nostro caso modifichiamo il metodo createTextChange e subito dopo la creazione del bottone showHide, impostiamo l'azione

showHide.setOnAction(e -> {
    if (txt.getOpacity() > 0.0) txt.setOpacity(0.0);
    else txt.setOpacity(1.0);
});

La proprietà OpacityProperty è una delle tante proprietà generali di un Node che possono essere impostate. L'opacità è un valore con virgola compreso tra 0.0 e 1.0 che determina il grado appunto di opacità con il quale il nodo è visualizzato, 0.0 significa completamente trasparente e 1.0 completamente opaco.

Ora vorremmo che il nome del bottone riflettesse il cambiamento di visibilità del testo, cioè quando il testo è visibile il nome dovrebbe essere Hide e quando è invisibile Show. Possiamo farlo modificando l'azione e anche il nome iniziale

Button showHide = new Button("Hide");
showHide.setOnAction(e -> {
    if (txt.getOpacity() > 0.0) {
        txt.setOpacity(0.0);
        showHide.setText("Show");
    } else {
        txt.setOpacity(1.0);
        showHide.setText("Hide");
    }
});

Proprietà

Vogliamo poter modificare la dimensione del testo visualizzato. Possiamo aggiungere un controllo Slider che permetta all'utente di cambiare agevolmente la dimensione della fonte. Dovendo aggiungere lo Slider conviene mettere tutti i controlli, che per adesso sono solo due, in un layout separato che li dispone su una riga orizzontale. Usiamo quindi un layout HBox.

private Parent createTextChange() {
    Text txt = new Text("Hello JavaFX");
    Button showHide = new Button("Hide");
    showHide.setOnAction(e -> {
        if (txt.getOpacity() > 0.0) {
            txt.setOpacity(0.0);
            showHide.setText("Show");
        } else {
            txt.setOpacity(1.0);
            showHide.setText("Hide");
        }
    });
    Slider size = new Slider(8, 40, 12); // Per la dimensione della fonte
    HBox hb = new HBox(showHide, size);  // Layout per i controlli
    hb.setAlignment(Pos.CENTER);
    VBox vb = new VBox(txt, hb);
    vb.setAlignment(Pos.CENTER);  // Allineamento dei nodi
    vb.setSpacing(30);            // Spazio tra i nodi
    return vb;
}

Nel costruttore dello Slider si può specificare, nell'ordine, il minimo e il massimo valore 8, 40 e il valore iniziale 12. Manca ancora la connessione tra lo Slider e la fonte del Text. Per prima cosa ci conviene scrivere un piccolo metodo di utilità per modificare la dimensione della fonte di un nodo Text2

/** Modifica la dimensione della fonte del nodo Text specificato.
 * @param t  un nodo Text
 * @param s  la nuova dimensione della fonte */
private static void tSize(Text t, double s) {
    t.setFont(new Font(t.getFont().getName(), s));
}

Un oggetto Font, in javafx.scene.text, rappresenta una fonte ed è immutabile. Il metodo Font getFont() di Text ritorna la fonte e il metodo void setFont(Font value) imposta la fonte.

Adesso vediamo come far sì che quando l'utente muove lo Slider la dimensione della fonte diventi uguale al valore impostato sullo Slider. Il metodo DoubleProperty valueProperty() di Slider ritorna un oggetto di tipo DoubleProperty del package javafx.beans.property. Così come molti altri tipi di proprietà un oggetto DoubleProperty implementa l'interfaccia Property<T>, con T uguale a Number, sempre del package javafx.beans.property. Questa interfaccia permette di creare dei legami diretti tra proprietà in modo tale che ogniqualvolta il valore di una proprietà cambia il valore dell'altra proprietà può essere automaticamente aggiornato di conseguenza. Per il momento non tratteremo questo meccanismo e invece consideriamo un meccanismo più generale che è veicolato da una super-interfaccia di Property<T> che si chiama ObservableValue<T> nel package javafx.beans.value. Tale interfaccia ha il metodo void addListener(ChangeListener<? super T> listener) che permette di aggiungere un listener (cioè un ascoltatore) che è invocato ogni volta che il valore della proprietà cambia. Il tipo di listener è dato dall'interfaccia funzionale ChangeListener<T>, nel package javafx.beans.value, nel nostro caso il tipo T è Number. L'unico metodo di ChangeListener<T> è void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) e questo è il metodo che deve essere implementato perché sarà invocato ogni volta che il valore del nostro Slider cambia. Quindi subito dopo la creazione dello Slider aggiungiamo le seguenti linee

tSize(txt, 12);
size.valueProperty().addListener((o,ov,nv) -> tSize(txt, (Double)nv));

Purtroppo il cast a Double è necessario perché la valueProperty dello Slider implementa ObservableValue<Number>.

Si può notare che facendo crescere e decrescere la dimensione del testo i controlli si spostano. Questo è normale perché il layout si aggiusta automaticamente quando i nodi in esso contenuti cambiano dimensione. Però potremmo non volere che i controlli si spostino. Per evitare ciò occorre porre il nodo che cambia dimensione, nel nostro caso il nodo Text, in un layout che ha una dimensione iniziale sufficientemente grande. Per questo possiamo usare un layout StackPane che dispone i nodi l'uno sopra l'altro, come in una pila. Quindi aggiungiamo le seguenti linee subito dopo la creazione del Text

StackPane sp = new StackPane(txt);
sp.setPrefHeight(80);

e sostituiamo la linea VBox vb = new VBox(txt, hb); con VBox vb = new VBox(sp, hb);.

Altri controlli

Aggiungiamo ora la possibilità di cambiare il nome delle fonte. Più precisamente il nome completo della fonte che comprende sia il nome della famiglia (ad es. Verdana o Monaco) che lo stile, se italico o normale, e il peso cioè più o meno in grassetto. Prima di tutto ci serve un metodo di utilità per poter modificare solamente il nome della fonte di un Text senza modificare la dimensione

/** Modifica il nome completo della fonte del nodo Text specificato.
 * @param t  un nodo Text
 * @param fname  il nuovo nome completo della fonte */
private static void tFName(Text t, String fname) {
    t.setFont(new Font(fname, t.getFont().getSize()));
}

Il controllo che conviene usare, dato il gran numero di possibili nomi di fonti, è il ComboBox<T> che è un bottone che premuto mostra una lista dei valori possibili, di tipo T, tra cui scegliere. Quindi aggiungiamo le seguenti linee

ComboBox<String> ff = new ComboBox<>();  // Per il nome della fonte
ff.setPrefWidth(100);
ff.getItems().addAll(Font.getFontNames());
ff.setValue(txt.getFont().getName());
ff.setOnAction(e -> tFName(txt, ff.getValue()));

e modifichiamo la linea HBox hb = new HBox(showHide, size); con HBox hb = new HBox(showHide, size, ff);. Il metodo statico List<String> getFontNames() di Font ritorna la lista dei nomi di tutte le fonti disponibili nella piattaforma corrente.

Volendo cambiare anche il colore del testo, possiamo usare il controllo specializzato ColorPicker che permette all'utente di scegliere un colore. Aggiungiamo quindi le seguenti linee

ColorPicker cp = new ColorPicker();     // Per il colore del testo
cp.setValue(Color.BLACK);
cp.setOnAction(e -> {
    txt.setStroke(cp.getValue());
    txt.setFill(cp.getValue());
});

Abbiamo anche esteso la larghezza della scena da 400 a 600. Come ultimo tocco aggiungiamo un campo per poter cambiare il testo. Per questo c'è il controllo TextField che permette all'utente di inserire una linea di testo.

TextField tf = new TextField("Hello JavaFX");  // Per cambiare il testo
tf.setMaxWidth(400);
tf.setOnAction(e -> txt.setText(tf.getText()));

L'azione scatta quando l'utente digita l'ENTER sul campo di testo. Il nodo tf lo aggiungiamo al VBox: VBox vb = new VBox(sp, hb, tf);.

In questa prima lezione su JavaFX abbiamo appena sfiorato la superficie di questa vasta libreria. Nelle prossime lezioni avremo modo di trattare molti altri strumenti.

Esercizi

[Layout]    Provare ad usare altri layout per disporre i controlli e il testo nella piccola applicazione che è stata presentata nella lezione. Ad esempio, invece di usare VBox e HBox usare BorderPane.

[FamilyAndStyle]    Modificare l'applicazione vista in modo che l'utente possa scegliere il nome di famiglia della fonte, lo stile e il peso indipendentemente l'uno dall'altro e non tutti insieme tramite il nome completo della fonte. Per fare ciò si può usare il metodo statico List<String> getFamilies() di Font che ritorna la lista delle famiglie di fonti disponibili. Inoltre il metodo statico Font font(String family, FontWeight weight, FontPosture posture, double size) permette di ottenere una fonte con le specificate caratteristiche.

[Colori]    Modificare l'applicazione vista in modo tale che l'utente possa scegliere i tre colori: background, contorno (stroke) e riempimento (fill) del testo.

[Rettangolo]    Modificare l'applicazione vista aggiungendo un bottone che mostra e nasconde un rettangolo nero che racchiude il testo ed è sullo sfondo. Il rettangolo deve evere una dimensione appena superiore a quella del testo e deve quindi essere aggiornato tutte le volte che il testo cambia dimensione.

[Rate]    Scrivere un'applicazione con tre campi per l'inserimento dell'importo del capitale, il numero totale di rate, il tasso di interesse annuo e un bottone che quando cliccato calcola e mostra l'importo della rata. Ogni campo dovrebbe avere un'etichetta appropriata.

[Conversioni]    Scrivere un'applicazione per effettuare conversioni tra unità di misura.

8 Mag 2015


  1. Se la classe è creata tramite l'IDE IntelliJ si può usare il template JavaFXApplication che prepara automaticamente l'estensione di Application, il main e il metodo start.

  2. Purtroppo JavaFX non offre nessun modo agevole per modificare una proprietà di una fonte lasciando invariate le altre (il nome, la dimensione, lo stile).