gianluca.aguzzi@unibo.it
angelo.filaseta@unibo.it
Libreria Java per la creazione di GUI per Rich Applications multi-piattaforma
SwipeEvent
), in funzione della piattaforma in cui l’applicazione è in esecuzioneJFXPanel
)SwingNode
)JFrame
di SwingWindow
Stage
Stage
può mostrare una sola Scene
alla volta: si imposta via Stage#setScene(Scene)
Scene#setRoot(Parent)
init
, start
, stop
, …)start(Stage)
che riceve lo stage primariopublic class App extends javafx.application.Application {
@Override
public void start(Stage stage) throws Exception {
Group root = new Group();
Scene scene = new Scene(root, 500, 300);
stage.setTitle("JavaFX Demo");
stage.setScene(scene);
stage.show();
}
}
import javafx.application.Application;
public class Main {
public static void main(String[] args) {
// App è la classe definita nella slide precedente
Application.launch(App.class, args);
}
}
main()
dentro la classe App
(che estende Application
) può risultare nel seguente errore: “Error: JavaFX runtime components are missing, and are required to run this application” (richiederebbe l’aggiunta di JavaFX al module path all’avvio dell’applicazione)main
in una classe separata da quella dell’applicazione JavaFXL’avvio mediante Application.launch(App.class)
comporta:
App
(la classe specificata che estende Application
)init()
start(javafx.stage.Stage)
dell’applicazionePlatform.exit()
Platform.isImplicitExit()
è true)stop()
dell’applicazioneNode
Parent
rappresenta nodi che possono avere figli (recuperabili via getChildren()
)Node
: SwingNode
, Canvas
, Parent
plugins {
java
application
id("com.github.johnrengelman.shadow") version "7.0.0"
}
repositories {
mavenCentral()
}
val javaFXModules = listOf("base", "controls", "fxml", "swing", "graphics" )
val supportedPlatforms = listOf("linux", "mac", "win") // All required for OOP
val javaFxVersion = 17
dependencies {
for (platform in supportedPlatforms) {
for (module in javaFXModules) {
implementation("org.openjfx:javafx-$module:$javaFxVersion:$platform")
}
}
}
application {
mainClass.set("it.unibo.samplejavafx.App")
}
App
) deve estendere la classe javafx.application.Application
void start(Stage primaryStage)
che è, di fatto, l’entry point dell’applicazione JavaFX (lo stage primario è creato dalla piattaforma)setScene()
)show()
main()
dell’applicazione Java, che deve chiamare Application.launch(App.class)
Ogni scena ha un root node relativo a una gerarchia di nodi descrivente la GUI
Ciascun nodo (componente) espone diverse proprietà osservabili (classe Property<T>
)
size
, posizion
, color
, …)text
, value
, …)controller
, …)Ciascun nodo genera eventi in relazione ad azioni dell’utente
public class Example1 extends Application {
@Override
public void start(Stage stage) throws Exception {
final Label lbl = new Label();
lbl.setText("Label text here...");
final Button btn = new Button();
btn.setText("Click me");
final HBox root = new HBox();
root.getChildren().add(btn);
root.getChildren().add(lbl);
stage.setTitle("JavaFX - Example 1");
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
}
Property<T>
è un ObservableValue<T>
(un valore ottenibile con getValue()
a cui possono essere associati dei ChangeListener
via remove/addListener
) scrivibile che può essere collegato/scollegato ad altri osservabili o proprietà attraverso
bind(ObservableValue<? extends T> observable)
/ bindBidirectional(Property<T> other)
unbind()
/ unbindBidirectional(Property<T> other)
xxx
di tipo T
ha (opzionalmente) getter/setter getXxx()
e setXxx()
, e un metodo xxxProperty()
che restituisce un oggetto Property<T>
TextField
offre getText():String
, setText(String)
, e textProperty():Property<String>
final TextField input = new TextField();
final Label mirror = new Label();
// connette la label con il valore del textfield
mirror.textProperty().bindBidirectional(input.textProperty());
mirror.setText("default");
Parent
(nodo che può avere nodi figli – cf. proprietà protected children
):
Group
(gestisce un insieme di figli; ogni trasformazione/effetto è applicata su ogni figlio)Region
(classe base per tutti i controlli UI e i layout)
Control
(classe base per tutti i controlli UI)layoutX
e layoutY
dei Node
)BorderPane
, HBox
/VBox
, TilePane
, GridPane
, FLowPane
, AnchorPane
, StackPane
ObservableList<Node> getChildren()
restituisce la lista di nodi figli di un qualunque nodo/layout
add(Node)
e addAll(Node...)
) e gestiti i componenti figliGroup g = new Group();
Label l1 = new Label("label");
Button b1 = new Button("a larger button");
Button b2 = new Button("small button");
g.getChildren().addAll(l1, b2, b3);
// Use binding to suitable place the components
b1.layoutXProperty().bind(l1.widthProperty().add(10));
b2.layoutXProperty().bind(b1.layoutXProperty()
.add(b1.widthProperty()).add(10));
g.setTranslateX(-5); // applies translation to all children
g.setEffect(new DropShadow()); // applies effect to all children
Text center = new Text("Center"); // ...
BorderPane bpane = new BorderPane(center, top, right, bottom, left);
bpane.setCenter(new Text("NewCenter"));
Button topLeft = new Button("Top Left");
AnchorPane.setTopAnchor(topLeft, 10.0); // 10px from the top edge
AnchorPane.setLeftAnchor(topLeft, 10.0); // 10px from the left edge
AnchorPane root = new AnchorPane(topLeft);
// An empty vertical TilePane with 5px horiz / 10px vertical spacing
TilePane tp2 = new TilePane(Orientation.VERTICAL, 5, 10);
tp2.setPrefRows(3);
tp.setPrefTileHeight(100);
for(Month m : Month.values()) { tp2.getChildren().add(new Label(m.name())); }
GridPane gp = new GridPane();
gp.setGridLinesVisible(true);
for(Month m : Month.values()) {
Label l = new Label(m.name());
gp.getChildren().add(l);
int columnIndex = (m.getValue()-1) / 4; int rowIndex = (m.getValue()-1) % 4;
GridPane.setConstraints(l, columnIndex, rowIndex);
// OR ALSO: gp.add(l, columnIndex, rowIndex);
}
javafx.geometry.Bounds
che espone:
getMinX()
, getMinY()
, getMinZ()
getWidth()
, getHeight()
, getDepth()
getMaxX()
… come getMinX()+getWidth()
…Scene
Region
ma non un Group
), allora il ridimensionamento della scena causerà un aggiustamento del layoutStage
Node
può essere “gestito” (managed) o meno: nel primo caso, il parent ne gestirà il posizionamento/dimensionamento (in base alla preferred size del nodo)Screen
(si veda slide più avanti), i bound degli schermi non-primari saranno relativi a quelli dello schermo primariojavafx.event.Event
) possono essere generati dall’interazione dell’utente con gli elementi grafici
consume()
)EventHandler<T extends Event>
deve implementare il metodo void handle(T)
setOn...()
Stage
all’event target)Button
btn.setOnMouseClicked(event -> {
lbl.setText("Hello, JavaFX World!");
});
// same as
btn.addEventHandler(ActionEvent.ACTION, e -> lbl.setText("Hello, JavaFX World!"));
public class App extends Application {
@Override
public final void start(final Stage mainStage) {
final Scene scene = new Scene(initSceneUI());
mainStage.setScene(scene);
mainStage.setTitle("JavaFX Example");
mainStage.show();
}
private Parent initSceneUI() {
final Label inputLbl = new Label("Input: ");
final TextField inputArea = new TextField();
final Button okBtn = new Button("Open a new Stage with the input data!");
okBtn.setOnMouseClicked(event -> {
new SecondStage(inputArea.getText()).show();
});
final BorderPane root = new BorderPane();
root.setRight(okBtn);
root.setLeft(inputLbl);
root.setCenter(inputArea);
BorderPane.setAlignment(inputLbl, Pos.CENTER_LEFT);
BorderPane.setAlignment(okBtn, Pos.CENTER_RIGHT);
return root;
}
}
public class SecondStage extends Stage {
private Label lbl;
public SecondStage(final String message) {
super();
setTitle("New Window...");
setScene(new Scene(initSceneUI(), 400, 200));
lbl.setText(message);
}
private Parent initSceneUI() {
lbl = new Label();
FlowPane root = new FlowPane();
root.setAlignment(Pos.CENTER);
root.getChildren().add(lbl);
return root;
}
}
public class Main {
public static void main(final String[] args) {
Application.launch(App.class, args);
}
}
Application
sono eseguiti (ad es. start
) oppure no (ad es. init
) su JFXATSwingUtilities.invokeLater
Screen s = Screen.getPrimary();
double dpi = s.getDpi();
Rectangle2D sb = s.getBounds();
Ractangle2D svb = s.getVisualBounds();
ObservableList<Screen> screenList = Screen.getScreens();
Ad es., per dimensionare lo stage
stage.xProperty().addListener(x -> {
Screen s = Screen.getScreensForRectangle(
new Rectangle2D(stage.getX(), stage.getY(), stage.getWidth(), stage.getHeight())
).get(0);
stage.setMinHeight(...);
stage.setMinWidth(...);
stage.setMaxHeight(s.getVisualBounds().getHeight()/2);
stage.setMaxWidth(s.getVisualBounds().getWidth()/2);
});
Linguaggio di markup basato su XML, utilizzato per descrivere la struttura della GUI (ovvero il scene graph)
Ogni file FXML (con estensione .fxml
) deve essere un file XML valido
<?xml version="1.0" encoding="UTF-8"?>
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml">
<children>
<Button fx:id="btn"
alignment="CENTER"
text="Say Hello!"
textAlignment="CENTER" />
<Label fx:id="lbl"
alignment="CENTER_LEFT"
text="Label Text Here!"
textAlignment="LEFT" />
</children>
</VBox>
Attraverso il tag <?import ... ?>
è possibile specificare i package in cui recuperare le classi dei componenti d’interesse
import
di JavaIl container principale (unico per il singolo file) deve specificare gli attributi xmlns
e xmlns:fx
fx
raccoglie nodi relativi al processamento interno del descrittore FXMLxmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
<children>
e </children>
fx:id
<TextField fx:id="textField1"/>
javafx.fxml.FXMLLoader
load(URL location)
javafx.fxml
(si veda ad es. la build Gradle)FXMLLoader
(esempio)layouts/main.fxml
contenente una descrizione valida per la GUI da caricareParent root = FXMLLoader.load(
ClassLoader.getSystemResource("layouts/main.fxml"));
public class Example3 extends Application {
@Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(ClassLoader.getSystemResource("layouts/main.fxml"));
Scene scene = new Scene(root, 500, 250);
stage.setTitle("JavaFX - Example 3");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Il riferimento ai componenti (nodi) inseriti nella GUI definita nel file FXML può essere recuperato tramite la scena a cui la GUI è stata collegata
Metodo Node lookup(String id)
Label lbl = (Label) scene.lookup("#lbl");
Button btn = (Button) scene.lookup("#btn");
btn.setOnMouseClicked(handler -> {
lbl.setText("Hello, FXML!");
});
lookup
richiede come parametro l’id specificato per il componente (attributo fx:id
nel file FXML) preceduto dal simbolo #fx:controller
con valore riferito al nome pienamente qualificato della classe che fungerà da controller@FXML
è possibile recuperare:
fx:id
) del nodo nel file FXML e il nome della variabile d’istanza annotata nella classe controllerpublic class CompleteExample extends Application {
@Override
public void start(Stage stage) throws Exception {
VBox root = FXMLLoader.load(ClassLoader.getSystemResource("layouts/main.fxml"));
Scene scene = new Scene(root, 500, 250);
stage.setTitle("JavaFX - Complete Example");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox
xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="it.unibo.oop.lab.javafx.UIController">
<children>
<Button fx:id="btn"
alignment="CENTER"
text="Say Hello!"
onMouseClicked="#btnOnClickHandler" />
<Label fx:id="lbl"
alignment="CENTER_LEFT"
text="Label Text Here!" />
</children>
</VBox>
public class UIController {
@FXML
private Label lbl;
@FXML
private Button btn;
@FXML
public void btnOnClickHandler() {
lbl.setText("Hello, World!");
}
}
ToggleButton
c’è la classe .toggle-button
, e per la proprietà blendMode
la proprietà CSS -fx-blend-mode
(notare prefisso -fx-
)#myButton { -fx-padding: 0.5em; } /* for an individual node */
.label { -fx-font-size: 30pt; } /* for all the labels */
### Applicazione dello stile
Programmaticamente
```java
Scene scene = new Scene(pane);
scene.getStylesheets().add(ClassLoader.getSystemResource("css/scene.css"));
HBox buttons = new HBox();
buttons.setStyle("-fx-border-color: red;");
buttons.getStyleClass().add("buttonrow");
Nel file FXML (ad esempio attaccandolo al nodo root)
<GridPane id="pane" stylesheets="css/scene.css"> ... </GridPane>
Model
: modello OO del dominio applicativo del sistemaView
: gestisce le interazioni con l’utente (input e output)Controller
: gestisce il coordinamento fra Model e ViewModel
: incapsula dati e logica relativi al dominio della applicazioneView
: incapsula la GUI, le sue sottoparti, e la logica di notificaController
: intercetta gli eventi della View, comanda le modifiche al modello, cambia di conseguenza la ViewModelInterface
: letture/modifiche da parte del ControllerViewObserver
: comandi inviati dalla view al controller (void
)ViewInterface
: comandi per settare la view, notifiche a fronte dei comandi (errori..)DrawNumber
DrawNumber
public interface DrawNumber {
void reset();
DrawResult attempt(int n);
}
public enum DrawResult {
YOURS_LOW("Your number is too small"),
YOURS_HIGH("Your number is too big"),
YOU_WON("You won"),
YOU_LOST("You lost");
private final String message;
DrawResult(final String message) {
this.message = message;
}
public String getDescription() {
return message;
}
}
DrawNumberObservable
public interface DrawNumberObservable extends DrawNumber {
Property<Integer> minProperty();
Property<Integer> maxProperty();
Property<Optional<Integer>> lastGuessProperty();
Property<Optional<DrawResult>> lastGuessResult();
Property<Integer> attemptsProperty();
Property<Integer> remainingAttemptsProperty();
}
DrawNumberImpl
public final class DrawNumberImpl implements DrawNumberObservable {
private final Property<Integer> choice;
private final Property<Integer> min;
private final Property<Integer> max;
private final Property<Integer> attempts;
private final Property<Integer> remainingAttempts;
private final Property<Optional<Integer>> lastGuess;
private final Property<Optional<DrawResult>> lastGuessResult;
private final Random random = new Random();
public DrawNumberImpl(final Configuration configuration) {
lastGuess = new SimpleObjectProperty<>(Optional.empty());
lastGuessResult = new SimpleObjectProperty<>(Optional.empty());
if (!configuration.isConsistent()) {
throw new IllegalArgumentException("The game requires a valid configuration");
}
this.min = new SimpleObjectProperty<>(configuration.getMin());
this.max = new SimpleObjectProperty<>(configuration.getMax());
this.attempts = new SimpleObjectProperty<>(configuration.getAttempts());
this.remainingAttempts = new SimpleObjectProperty<>(configuration.getAttempts());
this.choice = new SimpleObjectProperty<>(configuration.getAttempts());
this.reset();
}
...
DrawNumberImpl
public final class DrawNumberImpl implements DrawNumberObservable {
...
@Override
public void reset() {
this.remainingAttempts.setValue(this.attempts.getValue());
this.choice.setValue(this.min.getValue() + random.nextInt(this.max.getValue() - this.min.getValue() + 1));
}
@Override
public DrawResult attempt(final int guess) {
lastGuess.setValue(Optional.of(guess));
DrawResult result = lastGuessResult.getValue().orElse(DrawResult.YOU_LOST);
if (this.remainingAttempts.getValue() <= 0) {
result = DrawResult.YOU_LOST;
}
if (guess < this.min.getValue() || guess > this.max.getValue()) {
throw new IllegalArgumentException("The number is outside boundaries");
}
remainingAttempts.setValue(remainingAttempts.getValue() - 1);
if (guess > this.choice.getValue()) {
result = DrawResult.YOURS_HIGH;
}
if (guess < this.choice.getValue()) {
result = DrawResult.YOURS_LOW;
}
if (guess == this.choice.getValue()) {
result = DrawResult.YOU_WON;
}
lastGuessResult.setValue(Optional.of(result));
return result;
}
...
}
DrawNumberImpl
public final class DrawNumberImpl implements DrawNumberObservable {
@Override
public Property<Integer> minProperty() {
return min;
}
@Override
public Property<Integer> maxProperty() {
return max;
}
@Override
public Property<Integer> remainingAttemptsProperty() {
return remainingAttempts;
}
@Override
public Property<Integer> attemptsProperty() {
return attempts;
}
@Override
public Property<Optional<Integer>> lastGuessProperty() {
return lastGuess;
}
@Override
public Property<Optional<DrawResult>> lastGuessResult() {
return lastGuessResult;
}
DrawNumberView
public interface DrawNumberView {
void setObserver(DrawNumberViewObserver observer);
void start();
void numberIncorrect();
void result(DrawResult res);
void displayError(String message);
}
public interface DrawNumberViewObserver {
void newAttempt(int n);
void resetGame();
void quit();
}
DrawNumberViewImpl
public final class DrawNumberViewImpl implements DrawNumberView {
private static final String FRAME_NAME = "Draw Number App";
private static final String QUIT = "Quit";
private static final String RESET = "Reset";
private static final String GO = "Go";
private static final String NEW_GAME = ": a new game starts!";
private DrawNumberViewObserver observer;
private Stage frame;
private Label message;
private final DrawNumberObservable model;
private final Bounds initialBounds;
/**
* Initialises a view implementation for a drawnumber game.
* @param model
* @param initialBounds
*/
public DrawNumberViewImpl(final DrawNumberObservable model, final Bounds initialBounds) {
this.model = model;
this.initialBounds = initialBounds;
}
...
}
DrawNumberViewImpl
public final class DrawNumberViewImpl implements DrawNumberView {
...
public void start() {
frame = new Stage();
frame.setTitle(FRAME_NAME);
if (initialBounds != null) { frame.setX(initialBounds.getMinX()); frame.setY(initialBounds.getMinY()); }
final VBox vbox = new VBox();
final HBox playControlsLayout = new HBox();
final TextField inputNumber = new TextField();
final Button goButton = new Button(GO);
messageLabel = new Label();
final HBox gameControlsLayout = new HBox();
final Button resetButton = new Button(RESET);
final Button quitButton = new Button(QUIT);
final Label stateMessage = new Label();
setUpStateMessage(stateMessage.textProperty(), model);
goButton.setOnAction(e -> {
try { observer.newAttempt(Integer.parseInt(inputNumber.getText())); }
catch (NumberFormatException exception) {
MessageDialog.showMessageDialog(frame, "Validation error",
"You entered " + inputNumber.getText() + ". Provide an integer please...");
}
});
quitButton.setOnAction(e -> observer.quit());
resetButton.setOnAction(e -> observer.resetGame());
playControlsLayout.getChildren().addAll(inputNumber, goButton, messageLabel);
gameControlsLayout.getChildren().addAll(resetButton, quitButton);
vbox.getChildren().addAll(playControlsLayout, gameControlsLayout, stateMessage);
final int sceneWidth = 600;
final int sceneHeight = 200;
final Scene scene = new Scene(vbox, sceneWidth, sceneHeight);
this.frame.setScene(scene);
this.frame.show();
}
...
}
DrawNumberViewImpl
public final class DrawNumberViewImpl implements DrawNumberView {
...
public void setObserver(final DrawNumberViewObserver observer) { this.observer = observer; }
public void numberIncorrect() { displayError("Incorrect Number... try again"); }
public void result(final DrawResult result) {
switch (result) {
case YOURS_HIGH:
plainMessage(result.getDescription());
return;
case YOURS_LOW:
plainMessage(result.getDescription());
return;
case YOU_WON:
plainMessage(result.getDescription() + NEW_GAME);
break;
case YOU_LOST:
plainMessage(result.getDescription() + NEW_GAME);
break;
default:
throw new IllegalStateException("Unexpected result: " + result);
}
observer.resetGame();
}
private void plainMessage(final String message) { this.messageLabel.setText(message); }
public void displayError(final String message) { this.messageLabel.setText(message); }
private void setUpStateMessage(Property<String> stateMessage, DrawNumberObservable model) {
stateMessage.bind(new SimpleStringProperty("Min=").concat(model.minProperty())
.concat("; Max=").concat(model.maxProperty())
.concat("\nMaxAttempts=").concat(model.attemptsProperty())
.concat("; Remaining attempts=").concat(model.remainingAttemptsProperty())
.concat("\nLast guess:").concat(model.lastGuessProperty())
.concat("; Last outcome:").concat(model.lastGuessResult())
);
}
}
DrawNumberApp
public final class DrawNumberFXApplication extends Application implements DrawNumberViewObserver {
private DrawNumberObservable model;
private List<DrawNumberView> views;
public void init() {
final Parameters params = getParameters();
final String configFile = params.getRaw().stream().findFirst().orElseGet(() -> "examplemvc/config.yml");
final Configuration.Builder configurationBuilder = new Configuration.Builder();
// load from config ...
final Configuration configuration = configurationBuilder.build();
if (configuration.isConsistent()) {
this.model = new DrawNumberImpl(configuration);
} else {
displayError(".....");
this.model = new DrawNumberImpl(new Configuration.Builder().build());
}
views = Arrays.asList(
new DrawNumberViewImpl(model, new BoundingBox(0, 0, 0, 0)),
new DrawNumberViewImpl(model, null),
new PrintStreamView(System.out)
);
try {
views.add(new PrintStreamView("output.log"));
} catch (FileNotFoundException fnfe) {
System.out.println("Cannot find output file: " + fnfe.getMessage());
}
}
@Override
public void start(final Stage primaryStage) throws Exception {
for (final DrawNumberView view : views) {
view.setObserver(this);
view.start();
}
}
}
DrawNumberApp
public final class DrawNumberFXApplication extends Application implements DrawNumberViewObserver {
....
@Override
public void start(final Stage primaryStage) throws Exception {
for (final DrawNumberView view : views) {
view.setObserver(this);
view.start();
}
}
private void displayError(final String err) {
views.forEach(view -> view.displayError(err));
}
@Override
public void newAttempt(final int n) {
try {
final DrawResult result = model.attempt(n);
views.forEach(view -> view.result(result));
} catch (IllegalArgumentException e) {
for (final DrawNumberView view : views) {
view.numberIncorrect();
}
}
}
@Override
public void resetGame() {
this.model.reset();
}
@Override
public void quit() {
Platform.exit();
}
}
progettare le 3 interfacce
void
) chiamati da V, esprimono “azioni utente”void
) chiamati da C, esprimono richieste di visualizzazionela tecnologia scelta per le GUI sia interna a V, e mai menzionata altrove o nelle interfacce
implementare separatamente M, V e C, poi comporre e testare
in progetti reali, M, V e C si compongono di varie parti