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)javafx.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...()
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 */
Programmaticamente
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>
SwingNode
JFXPanel
SwingNode
e JFXPanel
si trovano nel modulo javafx.swing
javafx.application.Platform.runLater()
, per eseguire codice nel thread dedicato a JavaFXjavax.swing.SwingUtilities.invokeLater()
, per eseguire codice nel thread dedicato a Swingpublic static void main(final String[] args){
initMainJFrame(new JFrame("JFrame GUI"));
}
private static void initMainJFrame(final JFrame frame) {
final JButton button = new JButton();
button.setText("Launch JavaFX Scene");
button.addActionListener(event -> {
final JFXPanel jfxPanel = new JFXPanel();
Platform.runLater(() -> {
jfxPanel.setScene(new Scene(initJavaFXSceneUI(), 300, 300));
SwingUtilities.invokeLater(() -> {
final JFrame frameWithJavaFX = new JFrame("JFrame with JavaFX embedded!");
frameWithJavaFX.add(jfxPanel);
frameWithJavaFX.pack();
frameWithJavaFX.setVisible(true);
}); }); });
final JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
panel.add(button);
frame.setContentPane(panel);
frame.setSize(300, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
private static Parent initJavaFXSceneUI() {
final Label lbl = new Label();
lbl.setText("Hello, JavaFX World!");
final Button btn = new Button();
btn.setText("Say Hello");
btn.setOnMouseClicked(event -> {
lbl.setText("Hello from Button!");
});
final VBox root = new VBox();
root.getChildren().add(lbl);
root.getChildren().add(btn);
return root;
}
public final class JavaFXAppWithSwing extends Application {
@Override
public void start(final Stage primaryStage) throws Exception {
final SwingNode msg = new SwingNode();
SwingUtilities.invokeLater(() ->
msg.setContent(new JLabel("Hello by Swing JLabel")));
HBox pane = new HBox();
pane.getChildren().add(msg);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
// ...
}