JavaFX Schnell-Einstieg
2. Einfache App mit einer einzigen Klasse (und Stylesheet)
Hier eine ausführlich kommentierte Klasse (Erweiterung der App zum Probieren von JavaFX) und das dazugehörige StyleSheet, in der einige Grundkonzepte verwendet und erläutert werden (Passendes Icon muss selbst bereitgestellt werden):
Es wird auch lesend und schreibend auf Dateien zugegriffen (speichern der Fenster-Position in Properties-Datei). Details zur Arbeit mit Dateien siehe Lesen und Schreiben von Dateien
3. Java-Sourcecode zu Klasse 'my.pkg.jfxappmvnunmod01.RxJfxApp01'
package my.pkg.jfxappmvnunmod01;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.net.URL;
import java.util.Properties;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* JavaFX App mit BorderPane, TextField und Button, Preferenzen-Speicherung mit Properties-File.
* Stand 2022-02-14
*
* @author Maximilian_Renkin
*/
public class RxJfxApp01 extends Application {
private static final File CONF_FILE = new File("./app-conf.properties"); // in project basedir
private static final String CONFKEY_WINPOS_X = "winPos_X"; // Property key
private static final String CONFKEY_WINPOS_Y = "winPos_Y"; // Property key
private static final String DEFAULT_WINPOS_X_TXT = "60"; // Default Property value (text!!)
private static final String DEFAULT_WINPOS_Y_TXT = "120"; // Default Property value (text!!)
private Stage primaryStage;
private double winPosX = 0;
private double winPosY = 0;
private final Properties confProps = new Properties();
public RxJfxApp01() {
System.out.println("Constructor JfxApp() called");
}
public static void main(String[] args) {
System.out.println("main(...) called, now calling launch(...)");
launch(args); // ruft indirekt init(), start(...) und stop() auf
System.out.println("main(...) finished");
}
@Override // Optional Initialis. VOR Zugriff auf GUI (Dateien auslesen, etc.)!!
public void init() throws Exception {
super.init(); // als erste Aktion - noch kein Zugriff auf GUI-Elemente!
System.out.println("init() - initialization v23-0201a");
loadConfig();
}
@Override
public void stop() throws Exception {
// Aufraeumen - MIT Zugriff auf GUI
System.out.println("stop() called");
storeConfig();
super.stop(); // am Ende!
}
@Override
public void start(Stage primaryStage) throws Exception { // Hauptfenster als Param geliefert
// start(...) indirekt aufgerufen von launch(...), ihrerseits in main(...) aufgerufen
System.out.println("start(...) - the GUI activities - called)");
// Vom JavaFX Laufzeitsystem wird das Hauptfenster (hier 'primaryStage') bereitgestellt.
// Vom Laufzeitsystem bereitgestelltes Fenster als Attribut speichern (wird oft benötigt):
this.primaryStage = primaryStage;
configWindow(); // Titel, App-Icon, Fensterposition etc. setzen
// Erzeugen Basis-Container - entspricht in HTML ca. dem Bereich innerhalb <body>...</body>:
BorderPane root = new BorderPane(); // Wurzel-Container aller GUI-Elem., 'scene' übergeben
// BorderPane hat 5 Bereiche: Top, Bottom, Left, Right, Center, die separate Inhalte haben.
//
// Scene ist gesamter Window-Inhalt - UI-Elemente wie Menü, Hauptbereich -
// Entspricht analog zu Webseite im Wesentlichen allem innerhalb von <html>...</html>.
// Bei Erzeugung der Scene kann Basis-Container, Breite, Höhe des Fensters übergeben werden:
/////Scene scene = new Scene(root, 450, 250);
// oder es wird Größe durch die enthaltenen Komponenten bestimmt:
Scene scene = new Scene(root);
// Es können CSS-Stylesheets verwendet werden. Wenn diese im Java-Classpath liegen,
// lokalisiert man sie mit getClass().getResource("Pfad/im/Classpath").
// Das gelieferte URL-Objekt wird oft in Textform benötigt:
// urlObj.toExternalForm() oder gleichwertig: urlObj.toString()
// Hier wird die Stylesheet-Datei im selben Package erwartet, daher der "nackte" Dateiname:
// in Maven liegen Nicht-Java-Files im resources-Ordner mit oft gleicher Ordner-Hierarchie!
URL cssUrl = getClass().getResource("application.css");
String cssLink = cssUrl.toExternalForm(); // gleichwertig: cssUrl.toString();
scene.getStylesheets().add(cssLink);
System.out.format("CSS-File URL is '%s'%n", cssLink);
//
// Nun wird dieser Basis-Container dem Hauptfenster zugeordnet:
primaryStage.setScene(scene);
//
// Erstellen eines gestylten Label für den Titel ('Hello User!') im Top-Bereich:
Label lblHello = new Label("Hello User!"); // Erzeugen
// Label z.B. einfach mit CSS stylen - spezielle CSS-Namen mit Prefix '-fx-':
lblHello.setStyle("-fx-font-size: 2em;-fx-background-color:yellow; -fx-text-fill: blue;");
lblHello.setId("lblHello"); //Id od StyleClass setzen
lblHello.getStyleClass().add("lblHelloClass");
root.setTop(lblHello); // Variable nötig, da weitere Operation: setStyle(..)
root.setBottom(new Label("Das ist der Bottom-Bereich, für Status-Info o.ä.")); // direkt
//
// Zur Demo noch schnell ein Label im linken und rechten Bereich:
Label lbl4RootLeft = new Label("Linker\nBereich");
root.setLeft(lbl4RootLeft);
root.setMargin(lbl4RootLeft, new Insets(10));
Label lbl4RootRight = new Label("hier\nist\nrechts!"); // \n ... Zeilenschaltung
lbl4RootRight.setStyle("-fx-border-color: red;");
root.setRight(lbl4RootRight); // Label in Variabler kann später leicht geändert werden:
//lbl4RootRight.setMinWidth(100);
root.setMargin(lbl4RootRight, new Insets(10));
//
root.setCenter(composeMiniForm()); // hier wird das Mini-Formular im Center plaziert
//
primaryStage.show(); // erzeugtes Fenster wird erst nach expliziter Aufforderung angezeigt.
System.out.println("start(...) finished");
}
/*
@Override
public void start(Stage primaryStage) throws Exception { // Hauptfenster als Param geliefert
// start(...) indirekt aufgerufen von launch(...), ihrerseits in main(...) aufgerufen
System.out.println("start(...) - the GUI activities - called)");
// Vom JavaFX Laufzeitsystem wird das Hauptfenster (hier 'primaryStage') bereitgestellt.
// Vom Laufzeitsystem bereitgestelltes Fenster als Attribut speichern (wird oft benötigt):
this.primaryStage = primaryStage;
configWindow(); // Titel, App-Icon, Fensterposition etc. setzen
// Erzeugen Basis-Container - entspricht in HTML ca. dem Bereich innerhalb <body>...</body>:
BorderPane root = new BorderPane(); // Wurzel-Container aller GUI-Elem., 'scene' übergeben
// BorderPane hat 5 Bereiche: Top, Bottom, Left, Right, Center, die separate Inhalte haben.
//
// Scene ist gesamter Window-Inhalt - UI-Elemente wie Menü, Hauptbereich -
// Entspricht analog zu Webseite im Wesentlichen allem innerhalb von <html>...</html>.
// Bei Erzeugung der Scene kann Basis-Container, Breite, Höhe des Fensters übergeben werden:
/////Scene scene = new Scene(root, 450, 250);
// oder es wird Größe durch die enthaltenen Komponenten bestimmt:
Scene scene = new Scene(root);
// Es können CSS-Stylesheets verwendet werden. Hier wird die Stylesheet-Datei im selben
// Pkg(-Verzeichnis) wie die aktuelle Klasse (getClass()!!) erwartet. Geliefert wird eine
// Text-Repräsentation der Resource-URL (.toExternalForm()):
URL cssUrl = getClass().getResource("application.css");
String cssLink = cssUrl.toExternalForm(); // gleichwertig: cssUrl.toString();
scene.getStylesheets().add(cssLink);
System.out.format("CSS-File URL is '%s'%n", cssLink);
//
// Nun wird dieser Basis-Container dem Hauptfenster zugeordnet:
primaryStage.setScene(scene);
//
// Erstellen eines gestylten Label für den Titel ('Hello User!') im Top-Bereich:
Label lblHello = new Label("Hello User!"); // Erzeugen
// Label z.B. einfach mit CSS stylen - spezielle CSS-Namen mit Prefix '-fx-':
lblHello.setStyle("-fx-font-size: 2em;-fx-background-color:yellow; -fx-text-fill: blue;");
lblHello.setId("lblHello"); //Id od StyleClass setzen
lblHello.getStyleClass().add("lblHelloClass");
root.setTop(lblHello); // Variable nötig, da weitere Operation: setStyle(..)
root.setBottom(new Label("Das ist der Bottom-Bereich, für Status-Info o.ä.")); // direkt
//
// Zur Demo noch schnell ein Label im linken und rechten Bereich:
Label lbl4RootLeft = new Label("Linker\nBereich");
root.setLeft(lbl4RootLeft); // wenn Label nicht direkt ansprechbar sein muss
root.setMargin(lbl4RootLeft, new Insets(10));
Label lbl4RootRight = new Label("hier\nist\nrechts!"); // \n ... Zeilenschaltung
lbl4RootRight.setStyle("-fx-border-color: red;");
root.setRight(lbl4RootRight); // Label in Variabler kann später leicht geändert werden:
//lbl4RootRight.setMinWidth(100);
root.setMargin(lbl4RootRight, new Insets(10));
//
root.setCenter(composeMiniForm()); // hier wird das Mini-Formular im Center plaziert
//
primaryStage.show(); // erzeugtes Fenster wird erst nach expliziter Aufforderung angezeigt.
System.out.println("start(...) finished");
}
*/
private VBox composeMiniForm() {
// Hier erzeugen wir ein Mini-Formular mit Eingabefeld, Button und Ausgabe-Label.
// Wir planen zwei "Zeilen": eine mit Textfeld und Button, darunter die erzeugte
//.. Antwort. Wir verwenden eine VBox - und für die obere Zeile darin als erstes
//.. Element eine HBox.
//.. Das zweite, untere Element ist direkt ein Label, das die Antwort anzeigen kann:
VBox miniFormVbox = new VBox(); // dies ist der Container der zurückgeliefert wird
// noch min. Größe für VBox setzen (füllt verfügbaren Platz, wächst bei Bedarf automatisch):
miniFormVbox.setMinSize(300, 150);
// Bei Bedarf auch Obergrenze: miniFormVbox.setMaxSize(maxWidth, maxHeight);
miniFormVbox.setStyle("-fx-border-color: red;"); // wieder stylen
miniFormVbox.setPadding(new Insets(10));
//========
// die HBox für die erste Zeile:
HBox textPlusButtonHbox = new HBox(10.0);
miniFormVbox.getChildren().add(textPlusButtonHbox);
// Textfeld und Button:
String namePrompt = "Name eingeben"; // zu Prüfung nötig, ob schon Text eingegeben wurde
TextField nameTxfield = new TextField();
nameTxfield.setMinWidth(150);
nameTxfield.setPromptText(namePrompt);
// JavaFX CSS siehe [JavaFX CSS Reference Guide] (Suche z.B. nach 'derive'):
// https://openjfx.io/javadoc/17/javafx.graphics/javafx/scene/doc-files/cssref.html
nameTxfield.setStyle("-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);");
nameTxfield.setTooltip(new Tooltip("Hier bitte den Namen eingeben")); // nach 1s sichtbar
textPlusButtonHbox.getChildren().add(nameTxfield);
//
// danach einen Button hinzufügen (noch inaktiv):
Button answerButton = new Button("Sag was");
answerButton.setDefaultButton(true);
// für Abbruch-Button: answerButton.setCancelButton(true);
textPlusButtonHbox.getChildren().add(answerButton);
//========
// das AnswerLabel:
Label answerLabel = new Label("-- hier wird die Antwort sichtbar --");
miniFormVbox.getChildren().add(answerLabel);
// Button-Klick-Aktion als 'anonyme innere Klasse':
answerButton.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
if (nameTxfield.getText().isBlank()) {
answerLabel.setText("kein Name eingegeben");
} else {
answerLabel.setText("Hallo " + nameTxfield.getText() + ", wie gehts?");
}
}
});
// Alternativ Button-Klick-Reaktion per Lambda-Ausdruck:
// answerButton.setOnAction((ActionEvent event) -> { // Typ 'ActionEvent' optional!
// if (nameTxfield.getText().isBlank()) {
// answerLabel.setText("kein Name eingegeben");
// } else {
// answerLabel.setText("Hallo " + nameTxfield.getText() + ", wie gehts?");
// }
// });
return miniFormVbox;
}
/**
* Configure everything related to whole Window (title, app-icon, position, size, ...).
* Should be called after setting property 'primaryStage' and after calling 'loadConfig()',
* So needs to be called after getting primaryStage as parameter in 'start()'.
*/
public void configWindow() {
assert (this.primaryStage != null) : "needed field 'primaryStage' not set yet";
System.out.println("configWindow() called");
// der Fenstertitel wird gesetzt (wie HTML-<title>):
primaryStage.setTitle("Mein erstes JavaFX-Programm");
// Das App-Icon wird gesetzt:
String iconUrl = getClass().getResource("htlw5-logo1.png").toExternalForm();
primaryStage.getIcons().add(new Image(iconUrl));
// Die beim letzten Programmlauf gespeicherte Fenster-Position wird wieder verwendet:
String winPosX_txt = confProps.getProperty(CONFKEY_WINPOS_X, DEFAULT_WINPOS_X_TXT);
primaryStage.setX(Double.parseDouble(winPosX_txt));
String winPosY_txt = confProps.getProperty(CONFKEY_WINPOS_Y, DEFAULT_WINPOS_Y_TXT);
primaryStage.setY(Double.parseDouble(winPosY_txt));
}
/**
* Should be called in early state of App start - best within 'init()'.
*/
public void loadConfig() {
System.out.println("loadConfig() called");
assert (confProps != null) : "no confProps set"; // compact way to check preconditions
if (CONF_FILE.exists()) {
try (FileReader fr = new FileReader(CONF_FILE)) {
confProps.load(fr);
} catch (IOException e) {
System.out.format("Problem reading config file: %s", e.getMessage());
}
}
}
/**
* should be called at app exit - best within 'stop()'.
* Could store more settings - e.g. window size, current textfield content, etc.
*/
public void storeConfig() {
System.out.println("storeConfig() called");
assert (confProps != null) : "no confProps set"; // compact way to check preconditions
// collecting all current settings within confProps:
confProps.setProperty(CONFKEY_WINPOS_X, "" + primaryStage.getX());
confProps.setProperty(CONFKEY_WINPOS_Y, "" + primaryStage.getY());
//
try (FileWriter fw = new FileWriter(CONF_FILE)) {
confProps.store(fw, "JfxApp Config");
} catch (IOException e) {
System.out.format("Problem beim Schreiben von %s: %s", //
CONF_FILE.getAbsolutePath(), e.getMessage());
}
}
}
4. Inhhalt CSS, Bild-Datei für das App-Icon
Das Stylesheet jfx-app.css
liegt üblicherweise im selben Verzeichnis wie die Main-Klasse (u.u. in einem anderen Source-Dir – resources
mit selber Paclage-Struktur für Bilder, CSS, etc.).
Der beim Erstellen des Scene-Objekts übergebene Container root
erhält (unabhängig von seinem Variablennamen!) die CSS-Klasse .root
zugeordnet. In dieser CSS-Klasse lassen sich die Standardeinstellungen des Fensters (Stage) festlegen.
Die in der CSS-Klasse .root
definierten Styles werden von spezifischeren Definitionen und (am stärksten!) von direkt im Code mittels someNode.setStyle("-fx-…:wert;")
gesetzten Styles "verdrängt".
.root { /*Basis-CSS-Klasse - dem root-Element der Scene zugeordnet*/
-fx-font-size: 12pt;
-fx-font-family: "Courier New";
-fx-text-fill: blue;
-fx-background: rgb(32, 128, 128);
}
#lblHello {
-fx-font-family: Serif;
}
.lblHelloClass {
-fx-font-weight: bold;
}
#btn2 {
-fx-font-family: Helvetica, Arial, Sans-Serif;
}
Das Ergebnis als laufende App:

5. Einfache App mit Karteireiter-UI
Nun setzen wir ein UI-Element ein, dass wir in der Folge häufig nutzen werden: TabPane
.
Damit können wir eine Demo-App implementieren, die in unterschiedlichen Tabs unterschiedliche Funktionalitäten verwendet:
package my.application;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main2 extends Application {
@Override
public void start(Stage primaryStage) {
try {
TabPane tabPane = new TabPane(); // wird gleich als Basiscontainer der Scene verwendet
Scene scene = new Scene(tabPane, 400, 400);
primaryStage.setScene(scene);
// 3 Tab-Objekte (Karteireiter) erzeugen:
Tab tab1 = new Tab("Tab-1", new Label("Inhaltsbereich"));
Tab tab2 = new Tab("Tab-2");
tab2.setContent(new VBox());
((VBox) tab2.getContent()).getChildren().add(new Label("Inhalt von Tab-2: Element A"));
((VBox) tab2.getContent()).getChildren().add(new Label("Inhalt von Tab-2: Element B"));
// Tab 3:
VBox tab3Vbox = new VBox();
tab3Vbox.getChildren().addAll( //
new Label("Tab3-Label"), new TextField("Default-Text"), new Button("Button1"));
Tab tab3 = new Tab("Tab-3", tab3Vbox);
// oben definierte Tabs der TabPane hinzufügen:
tabPane.getTabs().addAll(tab1, tab2, tab3);
// Stylesheet einbinden:
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
// Anzeige des Fensters:
primaryStage.show();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Eine recht umfassende Quelle für CSS-Fragen ist der JavaFX 17 CSS Reference Guide des OpenJFX-Projekts.
Hier ein Screenshot der laufenden App mit aktivem Tab-3:

6. Karteireiter-UI-App mit separierten Karteireiter-Implementationen
Jetzt sind einige Grundlagen geschaffen und wir entwickeln ein Konzept, um in einer App kompakt und übersichtlich viele Themen umsetzen zu können:
package my.application;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.Map;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main3 extends Application {
private Map<String, LocalDate> birthDateMap = new HashMap<>();
public static void main(String[] args) {
launch(args); // ruft indirekt start(...) auf
}
@Override
public void init() throws Exception {
// Erzeugen einiger Demo-Daten:
birthDateMap.put("Evi", LocalDate.of(2002, 02, 20));
birthDateMap.put("Udo", LocalDate.of(1999, 12, 31));
}
@Override
public void start(Stage primaryStage) throws Exception {
// Aufbau der grundlegenden Strukturen:
BorderPane root = new BorderPane(); // Wurzel-Container für alle GUI-Elemente, wird unten 'scene' übergeben
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.setTitle("JavaFX Demo-App mit Tabs");
// Füllen des Content-Bereiches:
TabPane tabPane = new TabPane(); // zentrales Element der App
root.setCenter(tabPane); // platzieren der TabPane im Center der root-BorderPane
Tab tab0 = new Tab("Demo-Tab-0", new Label("Inhalt des Demo-Tab-0"));
Tab tab1 = new DemoTab1(this);
// Casting nötig, da normales Tab-Objekt Methode init() nicht kennt:
((DemoTab1) tab1).init(); // in der init()-Methode werden die Tab-Inhalte erzeugt
// man könnte statt obigem die init()-Methode auch im Konstruktor von DemoTab1 aufrufen
// alternativ: DemoTab1 tab1 = new DemoTab1(this); tab1.init();
tabPane.getTabs().addAll(tab0, tab1);
// Anzeige des Fensters:
primaryStage.show();
}
// Delegate-Methoden für birthDateMap (kontrollierbarer Zugriff auf die Map):
public int birthDateMapSize() {
return birthDateMap.size();
}
public boolean birthDateMapContainsKey(Object key) {
return birthDateMap.containsKey(key);
}
public LocalDate birthDateMapGet(Object key) {
return birthDateMap.get(key);
}
public LocalDate birthDateMapPut(String key, LocalDate value) {
return birthDateMap.put(key, value);
}
public LocalDate birthDateMapRemove(Object key) {
return birthDateMap.remove(key);
}
public boolean birthDateMapReplace(String key, LocalDate oldValue, LocalDate newValue) {
return birthDateMap.replace(key, oldValue, newValue);
}
public LocalDate birthDateMapReplace(String key, LocalDate value) {
return birthDateMap.replace(key, value);
}
}
package my.application;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
public class DemoTab1 extends Tab {
private Main3 main3Obj;
public DemoTab1(Main3 main3Obj) {
super();
this.main3Obj = main3Obj;
this.setText("Demo-Tab-1");
}
public void init() {
int birthDateMapSize = this.main3Obj.birthDateMapSize(); // Zugriff auf Methoden von Main3
Label lbl1 = new Label("In Tab " + this.getText() + ". Aktuell in BirthDateMap: " + birthDateMapSize);
setContent(lbl1);
}
}
Die laufende App:
