Metodologie di Programmazione: Lezione 19
Continuiamo l'esplorazione degli strumenti offerti da JavaFX per la costruzione di GUI. In questa lezione iniziamo l'implementazione di un mini Web Browser. Questo ci permetterà di vedere all'opera layout e molti controlli.
Il componente fondamentale per rendere pagine web è una cosiddetta web engine un sistema software piuttosto complesso e sofisticato capace di interpretare sia HTML/CSS che codice JavaScript, comunicare con server remoti, visualizzare il tutto (testo, layout, immagini, link, ecc.) e rispondere agli eventi prodotti dall'utente. JavaFX offre la componente WebView
, nel package javafx.scene.web
, che è una specie di finestra su la vera e propria web engine rappresentata da un oggetto di tipo WebEngine
, sempre nello stesso package. Quando una WebView
è creata, crea anche una WebEngine
che è disponibile tramite il metodo WebEngine getEngine()
. Iniziamo quindi a realizzare il nostro browser. La finestra principale contiene per ora solamente un campo di testo per poter digitare l'URL di una pagina web e una WebView
che scaricherà le pagine e le renderà.
package mp.gui;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
/** Un mini web browser */
public class Browser extends Application {
public static void main(String[] args) { launch(args); }
@Override
public void start(Stage primaryStage) {
Scene scene = new Scene(createUI(), 700, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
/** Crea e ritorna la componente principale della UI.
* @return la componente principale delle UI */
private Parent createUI() {
wView = new WebView();
WebEngine we = wView.getEngine();
TextField url = new TextField(); // Per immettere l'URL delle pagine
url.setOnAction(e -> we.load(url.getText()));
HBox hb = new HBox(url); // Per i controlli della web view
VBox vb = new VBox(hb, wView);
HBox.setHgrow(url, Priority.ALWAYS); // Si estende in orizzontale
VBox.setVgrow(wView, Priority.ALWAYS); // Si estende in verticale
return vb;
}
}
Eseguendolo e digitando l'URL http://www.di.uniroma1.it
,
Per il campo di testo url
abbiamo impostato l'azione che sarà eseguita ogni volta che si digita l'ENTER. Questa invoca il metodo void load(String u)
della web engine per accedere alla risorsa web con URL u
. Il metodo ritorna immediatamente perché il downloading della risorsa avviene in modo asincrono, vedremo fra poco come si può controllare tale task asincrono.
Abbiamo usato un layout HBox
per contenere il campo url
, ci aggiungeremo poi altri controlli. Infine abbiamo un VBox
per contenere i controlli e la web view. Siccome vogliamo che il campo url
possa estendersi in larghezza occupando tutto lo spazio disponibile abbiamo usato il metodo statico void setHgrow(Node child, Priority value)
di HBox
che permette di impostare tale comportamento con la costante Priority.ALWAYS
. In modo analogo impostiamo che la web view possa estendersi anche in verticale.
Ora vogliamo aggiungere due bottoni che permettono di navigare indietro e avanti nella sequenza delle pagine che sono state già visitate, come in un qualsiasi web browser. Ci aiuta la web engine che mantiene la history delle pagine in un oggetto WebHistory
che è ritornato dal metodo WebHistory getHistory()
. Quindi, nel metodo createUI
, possiamo ottenere la history e la lista dei suoi elementi una volta per tutte
WebHistory h = we.getHistory();
ObservableList<WebHistory.Entry> hList = h.getEntries();
Possiamo stare tranquilli perché sia getHistory
che il metodo ObservableList<WebHistory.Entry> getEntries()
ritornano sempre gli stessi oggetti per una fissata web engine, anche se gli elementi della lista cambieranno nel tempo. Adesso possiamo creare il bottone per andare indietro nella history,
Button back = new Button("<");
back.setOnAction(e -> h.go(-1));
Il metodo void go(int offset) throws IndexOutOfBoundsException
di WebHistory
fa sì che la web engine vada alla pagina della history all'indice curr + offset
, dove curr
è l'indice corrente. Quindi con -1
andiamo alla pagina precedente a quella corrente. Per evitare che quando non c'è una pagina precedente l'invocazione di go(-1)
vada in errore è necessario che il bottone back
sia disabilitato quando la pagina corrente ha indice 0. Quindi dobbiamo legare l'indice della pagina corrente alla disabilitazione del bottone back
. Più precisamente, se l'indice delle pagina corrente è 0, back
deve essere disabilitato, altrimenti deve essere abilitato. L'indice della pagina corrente è una proprietà ottenibile con il metodo ReadOnlyIntegerProperty currentIndexProperty()
di WebHistory
e la disabilitazione è una proprietà generale di Node
che si può ottenere con il metodo BooleanProperty disableProperty()
. Possiamo allora usare il metodo void bind(ObservableValue<? extends T> observable)
dell'interfaccia Property<T>
implementata da tutti i tipi di proprietà mutabili. Il metodo bind
prende in input un valore observable
che è appunto osservabile, quando observable
cambia anche il valore della proprietà è modificato allo stesso modo. Nel nostro caso abbiamo una proprietà i cui valori sono interi mentre la proprietà che vogliamo legare a questa ha valori booleani. Possiamo usare il metodo BooleanBinding isEqualTo(int other)
che è uno dei tanti metodi di utilità che le varie proprietà mettono a disposizione per trasformare un osservabile in un altro osservabile.
back.disableProperty().bind(h.currentIndexProperty().isEqualTo(0));
Possiamo creare analogamente anche il bottone per andare avanti nella history
Button forth = new Button(">");
forth.setOnAction(e -> h.go(1));
forth.disableProperty().bind(Bindings.createBooleanBinding(
() -> h.getCurrentIndex() >= hList.size() - 1,
h.currentIndexProperty()));
Qui il binding è un po' più complicato. Abbiamo dovuto usare il metodo statico BooleanBinding createBooleanBinding(Callable<Boolean> func, Observable... dependencies)
della classe Bindings
che quando uno degli osservabili dependencies
cambia valore l'oggetto func
è usato per determinare il nuovo valore booleano. Nel nostro caso quando la currentIndexProperty
cambia, il nuovo valore booleano è determinato eseguendo il test h.getCurrentIndex() >= h.getEntries().size() - 1
. Chiaramente si sarebbero potuti usare dei ChangeListener
al posto dei binding. Internamente l'intero framework dei binding è implementato tramite listener. I binding risultano convenienti quando le espressioni che legano due proprietà sono sufficientemente semplici, altrimenti è meglio usare i listener.
Rimane solamente di aggiungere i bottoni back
e forth
all'HBox
dei controlli,
HBox hb = new HBox(back, forth, url);
Eseguendo la nuova versione,
E possiamo navigare indietro e avanti tra le pagine già visitate.
I web browser non hanno solamente i bottoni per la navigazione ma possono mostrare la lista delle pagine precedenti e quella delle pagine successive. Potremmo quindi creare sul bottone back
un menu contestuale che mostra la lista delle pagine precedenti a quella corrente. Questo ci permetterà di vedere quanto sia facile in JavaFX creare e gestire menu contestuali. Un qualsiasi nodo può avere un menu contestuale. Ed è sufficiente usare un solo metodo void setOnContextMenuRequested(EventHandler<? super ContextMenuEvent> value)
di Node
. Il metodo imposta un gestore di eventi value
che sarà invocato ogni volta che l'utente chiede il menu contestuale su quel nodo. Tale gestore tipicamente crea (o lo prende da qualche parte) un appropriato menu contestuale e lo rende visibile. Ci manca di sapere come creare un menu contestuale. Il controllo ContextMenu
gestisce completamente un menu contestuale che in sostanza è una finestra di popup che contiene una lista di voci di menu. Le voci del menu sono gestite da controlli di tipo MenuItem
.
back.setOnContextMenuRequested(e -> {
int curr = h.getCurrentIndex();
if (curr < 1) return;
ContextMenu cm = new ContextMenu();
for (int i = curr - 1; i >= 0; i--) {
int offset = i - curr;
MenuItem mi = new MenuItem(hList.get(i).getUrl());
mi.setOnAction(v -> h.go(offset));
cm.getItems().add(mi);
}
cm.setAutoHide(true);
cm.show(back.getScene().getWindow(), e.getScreenX(), e.getScreenY());
});
Quindi il menu contestuale dopo essere stato creato viene riempito di MenuItem
ognuno dei quali ha un testo relativo all'URL di una pagina della history che precede la pagina corrente. Inoltre l'azione porta la web engine sulla pagina selezionata. Il metodo void show(Node anchor, double screenX, double screenY)
di ContextMenu
, rende visibile il menu contestuale alle specificate coordinate di schermo e queste possono essere ottenute dall'evento e
(sono precisamente le coordinate del mouse).
In modo del tutto analogo possiamo aggiungere un menu contestuale anche per il bottone forth
relativamente alle pagine successive a quella corrente.
forth.setOnContextMenuRequested(e -> {
int curr = h.getCurrentIndex();
if (curr >= h.getEntries().size() - 1) return;
ContextMenu cm = new ContextMenu();
for (int i = curr + 1; i < hList.size(); i++) {
int offset = i - curr;
MenuItem mi = new MenuItem(hList.get(i).getUrl());
mi.setOnAction(v -> h.go(offset));
cm.getItems().add(mi);
}
cm.setAutoHide(true);
cm.show(forth.getScene().getWindow(), e.getScreenX(), e.getScreenY());
});
Eseguendo questo nuova versione
Si sarà notato che quando si naviga a un'altra pagina cliccando su un link, il testo del campo url
non viene corrispondentemente modificato. Per poterlo fare dobbiamo essere avvertiti di quando un nuova pagina è caricata nella web engine. Come abbiamo già detto il downloading di una nuova pagina nella web engine è effettuato in modo asincrono. Più precisamente la web engine usa un oggetto di tipo Worker<V>
, nel package javafx.concurrent
, per eseguire in modo asincrono il task del downloading di una pagina. Il package javafx.concurrent
offre alcune classi e interfacce (di cui Worker<V>
è l'interfaccia base) che sono utili per implementare task che devono lavorare in background e che devono anche interagire con le componenti grafiche di JavaFX. Il metodo Worker<Void> getLoadWorker()
di WebEngine
, ritorna il Worker
usato dalla web engine. Un Worker
rende disponibile il suo stato tramite il metodo ReadOnlyObjectProperty<Worker.State> stateProperty()
. Quindi possiamo aggiungere un ChangeListener
a tale proprietà per essere avvertiti quando il Worker
termina con successo il downloading, cioè quando entra nello stato Worker.State SUCCEEDED
,
we.getLoadWorker().stateProperty().addListener((o, ov, nv) -> {
if (nv == Worker.State.SUCCEEDED) {
url.setText(we.getLocation());
} else if (nv == Worker.State.FAILED || nv == Worker.State.CANCELLED) { }
});
Quando il downloading termina con successo impostiamo il testo del campo url
alla nuova locazione.
L'implementazione del mini Web Browser continua nella prossima lezione.
[URL] Modificare l'azione del campo url
in modo tale che se la stringa digitata non contiene nessuno schema, inserisce lo schema di default http://
prima di sottometterlo alla WebView
.
[Zoom] Aggiungere un controllo al mini Web Browser che permette di fare lo zoom in e out della pagina visualizzata. Ad esempio può essere usato Spinner
o ChoiceBox<T>
.
15 Mag 2015