JavaFX FXML und SceneBuilder in IntelliJ

GUI-Erstellungswerkzeug
in Arbeit ...

1. Überblick

Ziel dieses Dokuments ist es, die grundlegenden Ansätze zur Erstellung einer JavaFX-Applikation unter Verwendung der deklarativen GUI-Definitionssprache FXML (ein XML-Format) zu erläutern und zu zeigen (hauptsächlich mit dem visuellen GUI-Erstellungs-Tool SceneBuilder) und etwas zu erläutern.

Infos zu Installation von Java, JavaFX, IntelliJ und SceneBuilder finden sich unter

TODO: in Arbeit

Eine vollständige Dokumentation zu FXML findet sich z.B unter Introduction to FXML | openjfx.io: JavaFX 19.

Anbei ein lauffähiges, sehr einfaches Maven-Projekt mit FXML:

Hier ein weiteres Mavan-Projekt, das die Verwendung mehrerer, dynamisch geladener FXML-Dateien zeigt:

2. Installation und Konfiguration von SceneBuilder

Download-URL: Scene Builder - Gluon

Version 20 ist auch mit älteren Java-Versionen (z.B. JDK-17) kompatibel!

Im folgenden wird vorausgesetzt, dass das IntelliJ-JavaFX-Plugin aktiv ist In IntelliJ ist unter Menü File  Settings…​, im angezeigten Settings-Formular links Languages & Frameworks  JavaFX findet sich ein Textfeld mit Beschriftung: "Path to SceneBuilder:". Dort ist der vollständige Pfad für den installierten SceneBuilder einzutragen (z.B. C:\Users\WerAuchImmer\AppData\Local\SceneBuilder\SceneBuilder.exe).

Auch ohne diese Konfiguration ist eine (allerdings etwas veraltete) embedded Version von SceneBuilder verfügbar. Diese erreicht man bei Öffnen einer FXML-Datei in IntelliJ als zweiter Karteireiter unten am Editor-Fenster (Text / Scene Builder).

3. Verwendung SceneBuilder

Damit ein UI-Element vom Controller aus vernünftig erreichbar ist, muss dem Element eine fx:id zugeordnet werden (rechts unten im Bereich Code). Diese ID wird zur Definition eines gleichnamigen Controller-Attributes (mit Annotation @FXML) verwendet.

Damit die Kooperation mit der Controller-Klasse funktioniert, muss dessen voll qualifizierte Klassenname angegeben werden (links unten im Bereich Controller)

Event-Handler-Methoden-Namen für die gewünschten Event-Arten sind ebenfalls unter Code festzulegen.

Nach Erzeugen der gewünschten UI-Elemente-Hierarchie und Festlegung der Controller-Klasse, aller gewünschten fx:id`s und Event-Handler-Methoden kann ein "Gerippe" der Controller-Klasse erstellt werden mit (ganz unten!):
View  Show Sample Controller Skeleton.
Im Formaular sollten beide Checkboxen unten (`Comments
, Full) angeklickt werden, um maximale Funktionalität und Kommentare zu erhalten.

4. FXML-Datei

Diese kann entweder im SceneBuilder oder innerhalb von IntelliJ erzeugt werden. Die Endung MUSS *.fxml sein, wobei dies in beiden Erzeugungswegen automatisch erfolgt bzw. vorgeschlagen wird. Sie sollte gemäß der empfohlenen Maven-Projekt-Struktur unter resources liegen. Empfehlenswert ist z.B. der Pfad des Projekt-Basispackage und darunter ein Ordner 'view'.

Benennung erfolgt häufig im Stil von StyleSheets: Ein korrespondierender Controller-Name MeinUiTeilController.java würde zu mein-ui-teil.fxml führen.

${project-dir}/src/my/demo/view/Base.fxml
<?xml version="1.0" encoding="UTF-8"?>                                     (1)

<?import javafx.scene.control.Button?>                                     (2)
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>

<BorderPane id="id_base-borderpane" fx:id="fxmlParent" minHeight="200.0" minWidth="300.0"
        xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="my.demo.view_ctlr.BaseController">                  (3)
  <center>                                                                 (4)
    <VBox alignment="CENTER" BorderPane.alignment="CENTER">                (5)
      <children>                                                           (6)
        <Button fx:id="btnDrueckenFuerMenue" mnemonicParsing="false"
                onAction="#menueZeigenAction" text="Drücken für Menü" />   (7)
        <Label prefHeight="62.0" prefWidth="192.0"
                text="Menü 'Settings / Inhaltsbereich aktivieren' wählen!"
                textAlignment="CENTER" wrapText="true" />                  (8)
      </children>
    </VBox>
  </center>
</BorderPane>
1 XML Definitionen
2 Import analog zu Java-Import
3 XML-Root-Element – erbt üblicherweise von javafx.scene.Node. Darin muss als Attribut fx:controller die zuständige Controller-Klasse voll qualifiziert mit hier fx:controller="my.demo.view_ctlr.BaseController" festgelegt sein. Im folgenden Beispiel wird für jede FXML-Datei eine fx:id="fxmlParent" der Wurzel-Pane erzwungen, wodurch elegantere und vereinheitlichte Einbindung der FXML-UIs möglich wird.
4 Properties der Klasse sind als Sub-Elemente in lowercase vorhanden, wenn nicht null (leer) (für BorderPane: center, top, bottom, left, right)
5 In diesen Sub-Elementen können weitere Pane- und Control-Elemente belidbig tief verschachtelt auftreten
6 children-Property der VBox
7 Darin weitere Sub-Elemente enthalten (hier 'Button')
8 und hier Label

Die FXML-Struktur ist also recht plausibel und mit etwas Übung auch recht gut manuell editierbar.

Dennoch ist das GUI-Erstellungstool SceneBuilder eine große Hilfe.

Dieser ist beispielsweise fähig, ein Gerippe für den dazugehörigen Controller zu erstellen:

/**
 * Sample Skeleton for 'demo1.fxml' Controller Class
 */
package my.demo.view_ctlr;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;

public class BaseController {

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;

    @FXML // fx:id="btnDrueckenFuerMenue"
    private Button btnDrueckenFuerMenue; // Value injected by FXMLLoader

    @FXML // fx:id="fxmlParent"
    private BorderPane fxmlParent; // Value injected by FXMLLoader

    @FXML
    void menueZeigenAction(ActionEvent event) {

    }

    @FXML // This method is called by the FXMLLoader when initialization is complete
    void initialize() {
        assert btnDrueckenFuerMenue != null : "fx:id=\"btnDrueckenFuerMenue\" was not injected: check your FXML file 'demo1.fxml'.";
        assert fxmlParent != null : "fx:id=\"fxmlParent\" was not injected: check your FXML file 'demo1.fxml'.";

    }
}

Alle mit der Annotation gekennzeichneten Attribute und Methoden sind mit den korrespondierenden Elementen (bei UI-Elementen wie Panes, Controls, etc. über die fx:id, die Event-Handler-Methoden und der Controller über ihren Namen) verbunden.

5. Beispiel-Applikation

Schon die obige, erläuterte FXML-Datei Base.fxml ist Teil der App. Die restlichen Dateien sind nachfolgend vollständig dargestellt.

Das Eclipse-Projekt ist hier zum Download verfügbar

5.1. Datei-Struktur dess Eclipse-Projekts VERALTET !!

${eclipse-project-dir}
  + src
    + my
      + demo
        + view
		  + Base.fxml
		  + ContentArea.fxml
		  + MenuBarArea.fxml
		  + MyDemoApp.fxml
		+ view_ctlr
		  + AccessToAll.java
		  + BaseController.java
		  + ContentAreaController.java
		  + MenuBarAreaController.java
		+ MyDemoApp.java
        + FxmlControllerIface.java
		+ my-demo-app.css

Nun folgen die einzelnen Klassen, FXML-Dateien und die CSS-Datei Hier folgt nun die Applikations-Klasse zum Start der App (im wesentlichen in Erstnutzungs-Reihenfolge):

  • Base.fxml …​ vorweg zur Erläuterung der FXML-Dateien verwendet. Definiert das "Begrüßungs-UI" der Applikation

  • FxmlApp.java …​ Klasse mit Main-Methode, startet die App

  • AccessToAll.java …​ zentralisiert Zugriffsmöglichkeit auf die Controller-Klassen, die jeweils Referenzen zu allen benötigten UI-Elementen innerhalb ihrer Root-Pane (FXML-Root-Element) bereitstellen

  • FxmlControllerIface.java …​ Interface, das eine vereinheitlichte Handhabung der Controller-Klassen ermöglicht. Alle Controller implementieren es.

  • BaseController.java …​ Controller des später modifizierten "Begrüßungs-UI" (Button-Event-Methode und fx:id der äußersten, unverändert bleibenden Root-BorderPane, deren 5 Bereiche später modifiziert werden)

  • MenuBarArea.fxml …​ Enthält eine HBox und darin den hinzuzfügenden MenuBar

  • MenuBarAreaController.java …​ der für den gesamten MenuBar zuständige Controller

  • ContentArea.fxml …​ die mittels Menü SettingsInhaltsbereich aktivieren den Bereich center ersetzende VBox mit der TabPane

  • ContentAreaController.java …​ der Controller dazu

  • my-demo-fxmlapp.css

5.2. Klasse "my.demo.FxmlApp"

package my.demo;

import javafx.application.Application;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import my.demo.view_ctlr.AccessToAll;
import my.demo.view_ctlr.BaseController;

public class FxmlApp extends Application {

    public FxmlApp() {

    }

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        //Schlecht: (kein Zugriff auf Controller):
        //          Parent root = loader.load(getClass().getResource("MyApp.fxml"));
        // besser - ermöglicht Zugriff auf Controller und damit UI-Elemente:
        // FXMLLoader loader = new FXMLLoader(getClass().getResource(FxmlApp.BASE_FXML));
        // Parent root = loader.load();
        // MUSS NACH loader.load() erfolgen - sonst wird 'null' !!!!! zurückgeliefert:
        // BaseController baseController = loader.<BaseController>getController();

        // Am besten: Alle Controller implementieren Interface FxmlControllerIface:
        // Damit ist es möglich, eine Methode 'createFromFxml(...)' zu implementieren, die 
        // das Laden und Konfigurieren einheitlich erledigt. 
        // Die Generic-Parameter (hier <BorderPane, BaseController>) werden nur für den 
        // Rückgabetyp benötigt und können weggelassen werden, da der Compiler den erforderlichen
        // Rückgabetyp bei der Zuweisung ohnehin prüfen kann/muss.
        BaseController baseCtlr = AccessToAll.singleton()
                .<BorderPane, BaseController>createFromFxml(AccessToAll.BASE_FXML);
        Parent root = baseCtlr.getFxmlParent();
        Scene scene = new Scene(root);
        AccessToAll.singleton().setScene(scene);
        AccessToAll.singleton().setBaseController(baseCtlr);

        scene.getStylesheets().add(getClass().getResource("my-demo-fxmlapp.css").toExternalForm());
        primaryStage.setScene(scene);
        primaryStage.show();
    }

}

Ist die Application-Klasse mit der main-Methode

5.3. Klasse "my.demo.view_ctlr.AccessToAll"

Klasse AccessToAll ermöglicht allen Controllern und weiteren Programmteilen, auf die UI-Elemente der anderen Controller zuzugreifen – z.B. kann der MenuBarAreaController auf Elemente des ContentArea zugreifen. Auf diese Weise kann jede Klasse, die Zugriff auf dieses (Singleton-) Objekt hat, alle per jeweils eigener FXML-Datei organisierten und in AccessToAll aufgenommenen Komponenten oder Funktionalitäten erreichen.

package my.demo.view_ctlr;

import java.io.IOException;

import javafx.event.Event;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Control;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.layout.Pane;
import my.demo.FxmlControllerIface;

public class AccessToAll { // könnte mann auch 'AppContext' o.ä. nennen
    public static final String BASE_FXML = "/my/demo/view/Base.fxml";
    public static final String MENUBAR_AREA_FXML = "/my/demo/view/MenuBarArea.fxml";
    public static final String CONTENT_AREA_FXML = "/my/demo/view/ContentArea.fxml";

    private static AccessToAll instance;
    private Scene scene;
    private BaseController baseController;
    private MenuBarAreaController menuBarAreaController;
    private ContentAreaController contentAreaController;

    public static AccessToAll singleton() {  // immer SELBES Objekt zurückliefern
        if (instance == null) {
            instance = new AccessToAll();  // kann nur ein einziges Mal instanziert werden
        }
        return instance;
    }

    private AccessToAll() { // Erzeugung Singleton: Verhindern, dass mit 'new AssignToAll()'
                            // neue Objekte erstellt werden können -> privater Konstruktor und
    } // Instanzieren nur mit Methode 'singleton(), wenn noch KEINE Instanz da'

    public Scene getScene() {
        return scene;
    }

    public void setScene(Scene scene) {
        this.scene = scene;
    }

    public BaseController getBaseController() {
        return baseController;
    }

    public void setBaseController(BaseController baseController) {
        assert baseController == null || baseController.getFxmlParent() != null
                : noFxmlParentMsg(BASE_FXML);
        this.baseController = baseController;
    }

    public MenuBarAreaController getMenuBarAreaController() {
        return menuBarAreaController;
    }

    public void setMenuBarAreaController(MenuBarAreaController menuBarAreaController) {
        assert menuBarAreaController == null || menuBarAreaController.getFxmlParent() != null
                : noFxmlParentMsg(MENUBAR_AREA_FXML);
        this.menuBarAreaController = menuBarAreaController;
    }

    public ContentAreaController getContentAreaController() {
        return contentAreaController;
    }

    public void setContentAreaController(ContentAreaController contentAreaController) {
        assert contentAreaController == null || contentAreaController.getFxmlParent() != null
                : noFxmlParentMsg(CONTENT_AREA_FXML);
        this.contentAreaController = contentAreaController;
    }

    private static String noFxmlParentMsg(String fxmlPath) {
        return "FXML-File '" + fxmlPath + "': fx:id des Wurzelelem. MUSS wegen "
                + "Controller-Basisklasse 'MyFxmlController' zwingend 'fxmlParent' sein";
    }

    /**
     * 
     * @param <P>      Typ der Wurzel-Pane der gegebenen FXML-Datei (BorderPane, HBox, VBox, ...)
     * @param <TT>     Typ des Controllers (BaseController, MenuBarAreaController, ...)
     * @param fxmlPath relativer Pfad der FXML-Datei (auch unter Windows '/' als Verzeichnis-Trenner
     *                 - Werte in AccessToAll als Konstante vorhanden (BASE_FXML, MENUBAR_AREA_FXML,
     *                 CONTENT_AREA_FXML)
     * 
     * @return C Generic Type für den zurückzuliefernden Controller
     */
    public <P extends Pane, TT extends FxmlControllerIface<P>> TT createFromFxml(String fxmlPath) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource(fxmlPath));
        try {     // loader.load MUSS ZUERST erfolgen, damit 'loader' alles weiß!
            loader.load();  // Parent parent = ... Rückgabewert nicht genutzt
        } catch (IOException ioe) {
            throw new IllegalArgumentException("Lade-Problem bei FXML-Datei", ioe);
        }
        return loader.getController();
    }

    // Methode, die eleganter in anderer Klasse enthalten wäre - Kompromiss wg. Kürze:
    public static String eventInfo(Event event) {
        String txt = "Event Typ " + event.getEventType() + ", ausgelöst von ";
        Object evtSrc = event.getSource();
        if (evtSrc instanceof MenuItem mi) {
            txt = "MenuItem '" + mi.getText() + "'";
        } else if (evtSrc instanceof Button bt) {
            txt = "Button '" + bt.getText() + "'";
        } else if (evtSrc instanceof TextField tf) {
            txt = "TextField '" + tf.getText() + "'";
        } else if (evtSrc instanceof Pane pn) {
            txt = "Pane - " + pn.toString();
        } else if (evtSrc instanceof Control cl) {
            txt = "Control - " + cl.toString();
        } else if (evtSrc instanceof Node nd) {
            txt = "Node - " + nd.toString();
        } else {
            txt = "Other Source Type - " + evtSrc.toString();
        }
        return txt;
    }
}

5.4. Interface "my.demo.FxmlControllerIface"

package my.demo;

import javafx.fxml.FXML;
import javafx.scene.layout.Pane;

public interface FxmlControllerIface<P extends Pane> {

    P getFxmlParent();

    @FXML
    void initialize();  // called by the FXMLLoader when initialization is complete
}

5.5. FXML-Datei "${project-dir}/src/my/demo/view/Base.fxml"

Siehe oben - diese wurde zur Erläuterung des FXML-Datei-Aufbaus genutzt

5.6. Klasse "my.demo.view_ctlr.BaseController"

/**
 * Sample Skeleton for 'MyDemoAppBase.fxml' Controller Class
 */

package my.demo.view_ctlr;

import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import my.demo.FxmlControllerIface;

public class BaseController implements FxmlControllerIface<BorderPane> {

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;

    @FXML
    private BorderPane fxmlParent;

    @FXML // fx:id="btnDrueckenFuerMenue"
    private Button btnDrueckenFuerMenue; // Value injected by FXMLLoader

    @FXML
    private Label lblInfoToDo;

    @FXML
    void menueZeigenAction(ActionEvent event) throws IOException {
        System.out.println("Menü wird instanziert und aktiviert - b");

        // MenuBarAreaController menuBarAreaCtlr = AccessToAll.singleton()
        // .<HBox, MenuBarAreaController>createFromFxml(AccessToAll.MENUBAR_AREA_FXML);
        // Explizite Rückgabe-Typ-Parameter korrekt, aber unnötig (Compiler kann's selbst):
        MenuBarAreaController menuBarAreaCtlr = AccessToAll.singleton()
                .createFromFxml(AccessToAll.MENUBAR_AREA_FXML);
        Parent menuBarArea = menuBarAreaCtlr.getFxmlParent();
        AccessToAll.singleton().setMenuBarAreaController(menuBarAreaCtlr);

        fxmlParent.setTop(menuBarArea);
        btnDrueckenFuerMenue.setDisable(true);
        lblInfoToDo.setDisable(false);
    }

    @Override
    @FXML // This method is called by the FXMLLoader when initialization is complete
    public void initialize() {
        assert fxmlParent != null : "fx:id=\"fxmlParent\" was not injected:"
                + " check your FXML file 'ContentArea.fxml'.";
        assert btnDrueckenFuerMenue != null : "fx:id=\"btnDrueckenFuerMenue\" was not injected:"
                + " check your FXML file 'MyDemoAppBase.fxml'.";
        assert lblInfoToDo != null : "fx:id=\"lblInfoToDo\" was not injected:"
                + " check your FXML file 'Base.fxml'.";
        // hier könnten weitere dynamische Setup-Aktivitäten erfolgen ...
    }

    @Override
    public BorderPane getFxmlParent() {
        return fxmlParent;
    }

}

5.7. FXML-Datei "${project-dir}/src/my/demo/view/MenuBarArea.fxml"

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.HBox?>

<HBox xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="my.demo.view_ctlr.MenuBarAreaController">
   <children>
      <MenuBar styleClass="menu-element">
        <menus>
          <Menu mnemonicParsing="false" styleClass="menu-element" text="File">
            <items>
              <MenuItem mnemonicParsing="false" onAction="#infoNotImplemented" styleClass="menu-element" text="Close" />
            </items>
          </Menu>
          <Menu mnemonicParsing="false" styleClass="menu-element" text="Edit">
            <items>
              <MenuItem mnemonicParsing="false" onAction="#infoNotImplemented" styleClass="menu-element" text="Delete" />
            </items>
          </Menu>
            <Menu mnemonicParsing="false" styleClass="menu-element" text="Settings">
              <items>
                <MenuItem mnemonicParsing="false" onAction="#setupContentArea" styleClass="menu-element" text="Inhaltsbereich aktivieren" />
                  <MenuItem mnemonicParsing="false" onAction="#tabAdd" text="Tab hinzufügen" />
              </items>
            </Menu>
          <Menu mnemonicParsing="false" styleClass="menu-element" text="Help">
            <items>
              <MenuItem mnemonicParsing="false" onAction="#infoNotImplemented" styleClass="menu-element" text="About" />
            </items>
          </Menu>
        </menus>
      </MenuBar>
   </children>
</HBox>

5.8. Klasse "my.demo.view_ctlr.MenuBarAreaController"

package my.demo.view_ctlr;

import java.io.IOException;
import java.net.URL;
import java.util.List;
import java.util.ResourceBundle;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import my.demo.FxmlControllerIface;

public class MenuBarAreaController implements FxmlControllerIface<HBox> {

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;

    @FXML
    private HBox fxmlParent;

    @FXML
    private MenuItem mniTabAdd;

    @FXML
    public void infoNotImplemented(ActionEvent event) {
        System.out.println("Noch nicht implementiert - " + AccessToAll.eventInfo(event));
    }

    @FXML
    public void setupContentArea(ActionEvent event) throws IOException {
        System.out.println(AccessToAll.eventInfo(event));

        ContentAreaController contentAreaCtlr = AccessToAll.singleton()
                .<VBox, ContentAreaController>createFromFxml(AccessToAll.CONTENT_AREA_FXML);
        AccessToAll.singleton().setContentAreaController(contentAreaCtlr);
        AccessToAll.singleton().getBaseController().getFxmlParent()
                .setCenter(contentAreaCtlr.getFxmlParent());
        mniTabAdd.setDisable(false);
    }

    @FXML
    public void showAbout(ActionEvent event) {
        String msg = "Die beste JavaFX-App - definitiv!";
        ButtonType btEnough = new ButtonType("Genug Info");
        ButtonType btShowMore = new ButtonType("Zeige mehr Info");
        ButtonType btShowAll = new ButtonType("Zeige komplette Info");
        Alert alertConfirm = new Alert(AlertType.CONFIRMATION, msg, //
                btEnough, btShowMore, btShowAll);
        alertConfirm.setHeaderText("RX-HeaderText");
        alertConfirm.setTitle("RX-Title");
        Optional<ButtonType> result = alertConfirm.showAndWait();
        if (result.get() == btEnough) {
            System.out.println("genug Info ... Ende!");
        } else if (result.get() == btShowMore) {
            System.out.println("mehr Info ... OK");
            Alert alertInfoMore = new Alert(AlertType.INFORMATION, "Hier mehr Info");
            alertInfoMore.show();
        } else {
            System.out.println("alle Info ... wird gemacht");
            Alert alertInfoAll = new Alert(AlertType.WARNING, "mehr gibt's nicht!");
            alertInfoAll.show();
        }
    }

    @FXML
    public void tabAdd(ActionEvent event) {
        System.out.println(AccessToAll.eventInfo(event));

        List<Tab> tabs = AccessToAll.singleton().getContentAreaController().getTbpContentTabPane()
                .getTabs();
        int nr = tabs.size() + 1;
        VBox contentVBox = new VBox();
        Tab newTab = new Tab("Tab " + nr, contentVBox);
        contentVBox.getChildren().add(new Label("Inhalt von Tab " + nr));
        tabs.add(newTab);
    }

    @FXML
    public void resetApp(ActionEvent event) throws IOException {
        System.out.println(AccessToAll.eventInfo(event));

        BaseController baseCtlr = AccessToAll.singleton()
                .<BorderPane, BaseController>createFromFxml(AccessToAll.BASE_FXML);
        Parent root = baseCtlr.getFxmlParent();
        AccessToAll.singleton().getScene().setRoot(root); // Scene erhält neue Rootcontainer-Instanz

        AccessToAll.singleton().setBaseController(baseCtlr); // neue BaseController-Instanz
        AccessToAll.singleton().setMenuBarAreaController(null); // alte Instanz nun ungültig
        AccessToAll.singleton().setContentAreaController(null); // detto
    }

    @Override
    @FXML // This method is called by the FXMLLoader when initialization is complete
    public void initialize() {
        assert fxmlParent != null : "fx:id=\"fxmlParent\" was not injected:"
                + " check your FXML file 'MenuBarArea.fxml'.";
        assert mniTabAdd != null : "fx:id=\"mniTabAdd\" was not injected:"
                + " check your FXML file 'MenuBarArea.fxml'.";
    }

    @Override
    public HBox getFxmlParent() {
        return fxmlParent;
    }

}

5.9. FXML-Datei "${project-dir}/src/my/demo/view/ContentArea.fxml"

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.VBox?>


<VBox xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/16" fx:controller="my.demo.view_ctlr.ContentAreaController">
   <children>
      <TabPane fx:id="tbpContentTabPane" prefHeight="200.0" prefWidth="349.0" tabClosingPolicy="UNAVAILABLE">
        <tabs>
          <Tab closable="false" text="Tab 1">
               <content>
                  <VBox prefHeight="169.0" prefWidth="271.0">
                     <children>
                        <Label text="Erster Tab, auf nicht schließbar gesetzt" />
                     </children>
                  </VBox>
               </content>
            </Tab>
        </tabs>
      </TabPane>
   </children>
</VBox>

5.10. Klasse "my.demo.view_ctlr.ContentAreaController"

/**
 * Sample Skeleton for 'ContentArea.fxml' Controller Class
 */

package my.demo.view_ctlr;

import java.net.URL;
import java.util.ResourceBundle;

import javafx.fxml.FXML;
import javafx.scene.control.TabPane;
import javafx.scene.layout.VBox;
import my.demo.FxmlControllerIface;

public class ContentAreaController implements FxmlControllerIface<VBox> {

    @FXML // ResourceBundle that was given to the FXMLLoader
    private ResourceBundle resources;

    @FXML // URL location of the FXML file that was given to the FXMLLoader
    private URL location;

    @FXML
    private VBox fxmlParent;

    @FXML // fx:id="tbpContentTabPane"
    private TabPane tbpContentTabPane; // Value injected by FXMLLoader

    @Override
    @FXML // This method is called by the FXMLLoader when initialization is complete
    public void initialize() {
        //super.initialize();
        assert fxmlParent != null : "fx:id=\"fxmlParent\" was not injected:"
                + " check your FXML file 'ContentArea.fxml'.";
        assert tbpContentTabPane != null : "fx:id=\"tbpContentTabPane\" was not injected:"
                + " check your FXML file 'ContentArea.fxml'.";
    }

    public TabPane getTbpContentTabPane() {
        return tbpContentTabPane;
    }

    @Override
    public VBox getFxmlParent() {
        return fxmlParent;
    }

}

5.11. Stylesheet "${project-dir}/src/my/demo/my-demo-fxmlapp.css"

Das Styling erfolgt komplett im Stylesheet

/* JavaFX CSS - Leave this comment until you have at least create one rule which uses -fx-Property */

.root {  /*Styling-Basisklasse*/
    -fx-font-size: 14pt;
    /*-fx-font-family: "Courier New";*/
    -fx-text-fill: blue;
    -fx-background-color: rgb(255, 255, 223);
}

.menu-element {
    -fx-border-width: 2px;
    -fx-border-style: solid;
    -fx-border-color: red;
    -fx-border-radius: 0.9em;
}

#base-borderpane {
    -fx-border-width: 2px;
    -fx-border-style: solid;
    -fx-border-color: darkviolet;
    -fx-border-radius: 1.0em;
}

5.12. FxmlApp in Aktion

ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b start
Abbildung 1. Direkt nach Start
ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b klick
Abbildung 2. Nach Anklicken des Buttons
ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b menu1
Abbildung 3. Nach Öffnen des Menüs zur selektierten Aktion
ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b menu2
Abbildung 4. Inhaltsbereich mit TabPane nun sichtbar, vor Tab-Hinzufügen
ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b tab2
Abbildung 5. Nun ist 2. Tab vorhanden
ScrShot20210502 RX Jfx ML Demo1 2021 04 27 b reset
Abbildung 6. Zuletzt Reset der App - danach wieder im Ausgangszustand

Der Konsolen-Output nach einem Zyklus, nach Reset bis zum Öffnen eines Tabs:

Menü wird instanziert und aktiviert - b
MenuItem 'Inhaltsbereich aktivieren'
MenuItem 'Tab hinzufügen'
MenuItem 'Reset App'
Menü wird instanziert und aktiviert - b
MenuItem 'Inhaltsbereich aktivieren'
MenuItem 'Tab hinzufügen'

6. Scribble

https://openjfx.io/javadoc/18/javafx.fxml/javafx/fxml/doc-files/introduction_to_fxml.html [Introduction to FXML | JavaFX 18] …​ 24.5.2022, 10:33:13

https://docs.oracle.com/javafx/2/best_practices/jfxpub-best_practices.htm [Implementing JavaFX Best Practices | JavaFX 2 Tutorials and Documentation] …​ 2021-05-18 Sehr gut, zeigt u.a. MVC mit FXML!

https://sodocumentation.net/javafx [javafx - Getting started with javafx | javafx Tutorial] …​ 2021-04-24

https://sodocumentation.net/javafx/topic/1580/fxml-and-controllers [javafx - FXML and Controllers | javafx Tutorial] …​ 2021-04-24

Links:

https://stackoverflow.com/questions/40246788/fxml-run-after-initialized [java - FXML: "Run after initialized" - Stack Overflow] …​ 2021-05-18 Beschreibt Procedere des FXMLLoaders etc.

https://edencoding.com/dependency-injection/ [Complete Guide To FXML Dependency Injection and Controller Initialisation – Eden Coding] …​ 2023-06-07

Weiteres …​