JavaFX komplexere Controls, Menüs und Ergänzungen

folgt ...
TODO:
* TabPane, Tab
* ListView<T>
* MenuBar, Menu, MenuItem, CheckMenuItem, SeparatorMenuItem
* ImageView

1. TabPane und Tab-Elemente

Die TabPane gehört trotz des suggestiven Namens zu den Controls. Auch ihre Elemente (Klasse Tab) sind Controls, obwohl ihre vorherrschende Verwendung die Bereitstellung eines Containers ist, was offenbar entscheidend für die Namensvergabe war.

Ein Verwendungsbeispiel findet sich unter theorie/a335_jfx-einstieg.adoc#id_jfx-tabview-app

Wie bei allen anderen Controls (erben von javafx.scene.control.Control) und Panes (erben von javafx.scene.layout.Pane) kann eine angepasste Version in Form einer Subklasse erstellt werden.

Das hat den Vorteil, dass nicht die instanzierende Klasse bzw. Methode (z.B. die Methode myApp.start()) aufgebläht und unnötig spezialisiert wird, sondern fast alle Details in der spezialisierten Subklasse von Tab festgelegt werden können.

Somit kann eine sehr allgemeine Klasse eine TabPane instanzieren und eine Tab-Hinzufüge-Methode mit der hinzuzufügenden Tab-Instanz (bzw. Instanz einer Tab-Subklasse) als Parameter bereitstellen, die von vielen Stellen (z.B. von MenuItems) aufgerufen werden kann, um benötigte Tabs zu erzeugen.

Eine sehr elegante und kompakte Form zum Generieren und Initialisieren eines solchen Tabs ist eine (statische) Factory-Methode in der jeweiligen Tab-Subklasse, die dann eine fertig konfigurierte Tab-Instanz zurückliefert.

Hier ein vollständiges Beispiel: Eclipse-Projekt-Source Jfx-Demo-2021-05-15.zip

Im Folgenden werden einige zentrale Konzepte gezeigt und erläutert:

//TODO: Beispiel hinzufügen

2. TextField mit TextFormatter

3. TextArea

in Arbeit ...

4. ListView

in Arbeit ...

5. ImageView

in Arbeit ...

Hier eine minimale JAvaFX-App, die das unterhalb des Source-Codes gezeigte Bild (ein Fraktal – vermutlich eine "Julia-Menge") in einem ImageView darstellt.
Das Bild kann im Web-Browser per "Bild/Grafik speichern unter …​" heruntergeladen werden und sollte im passenden (siehe Source-Code) Verzeichnis abgelegt werden:

// Package-Definition, Importe weggelassen!
public class JfxApp extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            VBox root = new VBox();
            // Verzeichnis "BILDER" muss im Projekt-Basisverzeichnis liegen (oder Pfad anpassen)
            // 6 Schritte zur Definition, Konfiguration und Nutzung eines ImageView:
            FileInputStream fis = new FileInputStream("BILDER/fraktal-1.jpg");  // Schritt 1
            Image img = new Image(fis);                                         // Schritt 2
            ImageView imgView = new ImageView(img);                             // Schritt 3
            imgView.setFitWidth(400);              // Bildbreite festlegen      // Schritt 4
            imgView.setPreserveRatio(true);        // Proportionen beibehalten  // Schritt 5
            root.getChildren().add(imgView);       // "Einbauen" des ImageView  // Schritt 6

            Scene scene = new Scene(root, 400, 400);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (FileNotFoundException e) {   // bei ungültigem Bild-Pfad
            throw new IllegalStateException("Bild-Datei nicht gefunden", e);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
Beispiel Fraktal/Julia-Menge

6. DatePicker

8. JavaFX MenuBar, Menu, MenuItem

in Arbeit ...

9. Beispiele

9.1. Beispiel Einzelklassen-App

Klasse 'my.pkg.JfxApp04a'
package my.pkg;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Properties;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 * App besteht nur aus einer einzigen selbstgeschriebenen Klasse.
 * @author mxrenkin
 *
 */
public class JfxApp04a extends Application {
    private static final File CONF_FILE = new File("./app-conf.properties"); // in project basedir
    private static final String CONF_WINPOS_X = "winPos_X";    // Property key
    private static final String CONF_WINPOS_Y = "winPos_Y";    // --''--
    private static final String DEFAULT_WINPOS_X_TXT = "60";   // Default Property value (text!!)
    private static final String DEFAULT_WINPOS_Y_TXT = "120";  // --''--

    public static final String MENU_FILE = "menu-file";
    public static final String MENU_TABS = "menu-tabs";
    public static final String MENU_HELP = "menu-help";

    private final Properties confProps = new Properties();
    private Stage primaryStage;
    private TabPane tabPane;
    private MenuBar menuBar;
    private Menu tabMenu;

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

    public void init() throws Exception {
        super.init();
        System.out.println("init() - initialization (BEFORE access to GUI) called");
        loadConfig();
    }

    @Override
    public void stop() throws Exception {
        // Aufraeumen - MIT Zugriff auf GUI
        System.out.println("stop() called");
        storeConfig();
        super.stop();
    }

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        configWindow();

        BorderPane root = new BorderPane();

        Scene scene = new Scene(root /*, 400, 400*/);
        primaryStage.setScene(scene);

        // Stylesheet einbinden:
        String cssUrl = getClass().getResource("jfx-app-04.css").toExternalForm();
        scene.getStylesheets().add(cssUrl);
        System.out.format("CSS-File URL is '%s'%n", cssUrl);

        // MenuBar in Top-Bereich setzen:

        root.setTop(this.menuBar = composeMenuBar());

        // tabPane als BasisContainer des Center setzen:
        root.setCenter(this.tabPane = composeTabPane(2));  // Tab-Nr 1, 2 oder 3 aktivieren

        // Anzeige des Fensters:
        primaryStage.show();
    }

    public MenuBar composeMenuBar() {
        MenuBar menuBar = new MenuBar();
        //
        Menu fileMenu = new Menu("Datei");
        MenuItem mniCreateFile = new MenuItem("Neue Datei");
        fileMenu.getItems().add(mniCreateFile);
        tabMenu = new Menu("Tabs");  // InstanceVar, damit dynamisch änderbar!
        //tabMenu.setId("tabMenu");
        //
        Menu helpMenu = new Menu("Hilfe");
        MenuItem mniAboutBox = new MenuItem("Über diese App ...");
        helpMenu.getItems().add(mniAboutBox);
        mniAboutBox.setOnAction(event -> {
            System.out.println("MenuItem 'aboutBox' aktiviert");
            Alert alert = new Alert(AlertType.INFORMATION);
            alert.setTitle("App-Info");
            alert.setHeaderText("Was ist diese App");
            alert.setContentText("Das ist die weitaus faszinierendste App");
            alert.initOwner(primaryStage);
            alert.showAndWait();
        });
        //
        menuBar.getMenus().addAll(fileMenu, tabMenu, helpMenu);

        return menuBar;
    }

    public TabPane composeTabPane(int activeTabNr) {
        assert (activeTabNr >= 1 && activeTabNr <= 4)
                : "invalid activeTabNr (1..4): %d".formatted(activeTabNr);
        tabPane = new TabPane();

        // Tab 1: simpel, enthält nur 1 Label
        Tab tab1 = new Tab("Tab-1", new Label("Inhaltsbereich"));

        // Tab 2: tab2Vbox Referenz erzeugen mit tab2 auslesen und casten - (VBox)tab2.getContent():
        Tab tab2 = new Tab("Tab-2");
        tab2.setContent(new VBox());
        VBox tab2Vbox = (VBox) tab2.getContent();
        tab2Vbox.getChildren().add(new Label("Inhalt von Tab-2: Element A"));
        tab2Vbox.getChildren().add(new Label("Inhalt von Tab-2: Element B"));

        // Tab 3: tab3Vbox erst erzeugen, dann in TabPane aufnehmen:
        VBox tab3Vbox = new VBox();
        //tab3Vbox.setStyle("-fx-padding: 10 10 10 10;");
        tab3Vbox.getChildren().addAll( //
                new Label("Tab3-Label"), new TextField("Default-Text"), new Button("Button1"));
        Tab tab3 = new Tab("Tab-3", tab3Vbox);

        // Tab 4: enthält TextArea:
        Tab tab4 = new Tab("Tab-4", setupTextArea());
        // oben definierte Tabs der TabPane hinzufügen, tab mit 'activeTabNr' aktivieren:
        tabPane.getTabs().addAll(tab1, tab2, tab3, tab4);
        activateTab(activeTabNr);

        //        TextField tf1 = new TextField();
        //        tf1.setOn

        tab1.setOnSelectionChanged((Event event) -> {
            if (tab1.isSelected()) {
                System.out.println("Tab 1 wurde jetzt aktiviert");
                //TODO: Aktivieren des Datei-Laden/Speichern Menüs
            } else {
                //TODO: Disablen des Datei-Laden/Speichern Menüs
                System.out.println("Tab 1 ist jetzt nicht mehr selektiert");
            }
        });
        tab2.setOnSelectionChanged((Event event) -> {
            if (tab2.isSelected()) {
                System.out.println("Tab 2 wurde jetzt aktiviert");
            } else {
                System.out.println("Tab 2 ist jetzt nicht mehr selektiert");
            }
        });
        tab3.setOnSelectionChanged((Event event) -> {
            if (tab3.isSelected()) {
                System.out.println("Tab 3 wurde jetzt aktiviert");
            } else {
                System.out.println("Tab 3 ist jetzt nicht mehr selektiert");
            }
        });

        tab4.setOnSelectionChanged((Event event) -> {
            if (tab4.isSelected()) {
                System.out.println("Tab 4 wurde jetzt aktiviert");
                MenuItem mniSaveNote = new MenuItem("Notiz speichern");
                MenuItem mniLoadNote = new MenuItem("Notiz laden");
                tabMenu.getItems().addAll(mniLoadNote, mniSaveNote);
                mniLoadNote.setOnAction((ActionEvent evt) -> {
                    loadNote();
                });
            } else {
                System.out.println("Tab 4 ist jetzt nicht mehr selektiert");
                tabMenu.getItems().clear();  // Entfernen der MenuItem Objekte
            }
        });

        return tabPane;
    }

    public void activateTab(int tabNr) {  // tabNr 1, 2, 3, 4
        tabPane.getSelectionModel().select(tabNr - 1);  // tab mit tabNr aktiv machen
    }

    public TextArea setupTextArea() {  // in tab4 verwendet
        TextArea textArea = new TextArea();
        textArea.setEditable(true);
        textArea.setWrapText(true);
        return textArea;
    }

    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(CONF_WINPOS_X, DEFAULT_WINPOS_X_TXT);
        primaryStage.setX(Double.parseDouble(winPosX_txt));
        String winPosY_txt = confProps.getProperty(CONF_WINPOS_Y, DEFAULT_WINPOS_Y_TXT);
        primaryStage.setY(Double.parseDouble(winPosY_txt));
    }

    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());
            }
        }
    }

    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(CONF_WINPOS_X, "" + primaryStage.getX());
        confProps.setProperty(CONF_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());
        }
    }

    public void loadNote() {
        System.out.println("loadNote() called");
    }
}

9.2. Beispiel Beginn sinnvolle Funktionalitäts-Gliederung

9.2.1. Klasse 'my.pkg.JfxApp04b'

package my.pkg;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.layout.Border;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import my.pkg.duty.QuickNote;
import my.pkg.engine.Engine;

/**
 * App besteht aus mehreren selbstgeschriebenen Klassen.
 *
 * @author mxrenkin
 *
 */
public class JfxApp04b extends Application {
    private static final File CONF_FILE = new File("./app-conf.properties"); // in project basedir
    private static final String CONF_WINPOS_X = "winPos_X";    // Property key
    private static final String CONF_WINPOS_Y = "winPos_Y";    // --''--
    private static final String DEFAULT_WINPOS_X_TXT = "60";   // Default Property value (text!!)
    private static final String DEFAULT_WINPOS_Y_TXT = "120";  // --''--

    public static final String MENU_FILE = "menu-file";
    public static final String MENU_TABS = "menu-tabs";
    public static final String MENU_HELP = "menu-help";

    //    private static JfxApp04b gui;
    private Engine engine;
    private final Properties confProps = new Properties();
    private Stage primaryStage;
    private TabPane tabPane;
    private MenuBar menuBar;
    private Menu tabMenu;
    private int nextTabId = 1;
    private List<QuickNote> quickNotes = new ArrayList<>();

    public JfxApp04b() {
        engine = new Engine();
    }

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

    //    private static JfxApp04b gui() {
    //        return gui;
    //    }

    public Engine getEngine() {
        return engine;
    }

    public void init() throws Exception {
        super.init();
        System.out.println("init() - initialization (BEFORE access to GUI) called");
        loadConfig();
    }

    @Override
    public void stop() throws Exception {
        // Aufraeumen - MIT Zugriff auf GUI
        System.out.println("stop() called");
        storeConfig();
        super.stop();
    }

    @Override
    public void start(Stage primaryStage) {
        this.primaryStage = primaryStage;
        configWindow();

        BorderPane root = new BorderPane();

        Scene scene = new Scene(root /*, 400, 400*/);
        primaryStage.setScene(scene);

        // Stylesheet einbinden:
        String cssUrl = getClass().getResource("jfx-app-04.css").toExternalForm();
        scene.getStylesheets().add(cssUrl);
        System.out.format("CSS-File URL is '%s'%n", cssUrl);

        // MenuBar in Top-Bereich setzen:

        root.setTop(this.menuBar = composeMenuBar());

        // tabPane als BasisContainer des Center setzen:
        root.setCenter(this.tabPane = composeTabPane(2));  // Tab-Nr 1, 2 oder 3 aktivieren

        // Anzeige des Fensters:
        primaryStage.show();
    }

    public MenuBar composeMenuBar() {
        MenuBar menuBar = new MenuBar();
        //
        Menu fileMenu = new Menu("Datei");
        MenuItem mniCreateFile = new MenuItem("Neue Datei");
        fileMenu.getItems().add(mniCreateFile);
        tabMenu = new Menu("Tabs");  // InstanceVar, damit dynamisch änderbar!
        MenuItem mniCreateNoteTab = new MenuItem("Neuer Notiz-Tab");
        tabMenu.getItems().add(mniCreateNoteTab);
        mniCreateNoteTab.setOnAction((ActionEvent event) -> {
            System.out.println("MenuItem 'mniCreateNoteTab' aktiviert");
            QuickNote qNote = new QuickNote(this, nextTabId++, tabPane, tabMenu);
            quickNotes.add(qNote);
            qNote.setupMyTab(-1);
        });

        //tabMenu.setId("tabMenu");
        //
        Menu helpMenu = new Menu("Hilfe");
        MenuItem mniAboutBox = new MenuItem("Über diese App ...");
        helpMenu.getItems().add(mniAboutBox);
        mniAboutBox.setOnAction(event -> {
            System.out.println("MenuItem 'aboutBox' aktiviert");
            Alert alert = new Alert(AlertType.INFORMATION);
            alert.setTitle("App-Info");
            alert.setHeaderText("Was ist diese App");
            alert.setContentText("Das ist die weitaus faszinierendste App");
            alert.initOwner(primaryStage);
            alert.showAndWait();
        });
        //
        menuBar.getMenus().addAll(fileMenu, tabMenu, helpMenu);

        return menuBar;
    }

    public TabPane composeTabPane(int activeTabNr) {
        assert (activeTabNr >= 1 && activeTabNr <= 4)
                : "invalid activeTabNr (1..4): %d".formatted(activeTabNr);
        tabPane = new TabPane();

        // Tab 1: simpel, enthält nur 1 Label
        Tab tab1 = new Tab("Tab-" + nextTabId++, new Label("Inhaltsbereich"));
        tab1.setClosable(false);

        // Tab 2: tab2Vbox Referenz erzeugen mit tab2 auslesen und casten - (VBox)tab2.getContent():
        Tab tab2 = new Tab("Tab-" + nextTabId++);
        tab2.setClosable(false);
        tab2.setContent(new VBox());
        VBox tab2Vbox = (VBox) tab2.getContent();
        tab2Vbox.getChildren().add(new Label("Inhalt von Tab-2: Element A"));
        tab2Vbox.getChildren().add(new Label("Inhalt von Tab-2: Element B"));

        // Tab 3: tab3Vbox erst erzeugen, dann in TabPane aufnehmen:
        VBox tab3Vbox = new VBox();
        //tab3Vbox.setStyle("-fx-padding: 10 10 10 10;");
        tab3Vbox.getChildren().addAll( //
                new Label("Tab3-Label"), new TextField("Default-Text"), new Button("Button1"));
        Tab tab3 = new Tab("Tab-" + nextTabId++, tab3Vbox);
        tab3.setClosable(false);

        // Tab 4: enthält TextArea:
        Tab tab4 = new Tab("Tab-" + nextTabId++, setupTextArea());
        tab4.setClosable(false);
        // oben definierte Tabs der TabPane hinzufügen, tab mit 'activeTabNr' aktivieren:
        tabPane.getTabs().addAll(tab1, tab2, tab3, tab4);
        activateTab(activeTabNr);

        //        TextField tf1 = new TextField();
        //        tf1.setOn

        tab1.setOnSelectionChanged((Event event) -> {
            if (tab1.isSelected()) {
                System.out.println("Tab 1 wurde jetzt aktiviert");
                //TODO: Aktivieren des Datei-Laden/Speichern Menüs
            } else {
                //TODO: Disablen des Datei-Laden/Speichern Menüs
                System.out.println("Tab 1 ist jetzt nicht mehr selektiert");
            }
        });
        tab2.setOnSelectionChanged((Event event) -> {
            if (tab2.isSelected()) {
                System.out.println("Tab 2 wurde jetzt aktiviert");
            } else {
                System.out.println("Tab 2 ist jetzt nicht mehr selektiert");
            }
        });
        tab3.setOnSelectionChanged((Event event) -> {
            if (tab3.isSelected()) {
                System.out.println("Tab 3 wurde jetzt aktiviert");
            } else {
                System.out.println("Tab 3 ist jetzt nicht mehr selektiert");
            }
        });
        MenuItem mniSaveNote = new MenuItem("Notiz speichern");
        MenuItem mniLoadNote = new MenuItem("Notiz laden");
        tab4.setOnSelectionChanged((Event event) -> {
            if (tab4.isSelected()) {
                System.out.println("Tab 4 wurde jetzt aktiviert");
                tabMenu.getItems().addAll(mniSaveNote, mniLoadNote);
                mniLoadNote.setOnAction((ActionEvent evt) -> {
                    loadNote();
                });
            } else {
                System.out.println("Tab 4 ist jetzt nicht mehr selektiert");
                tabMenu.getItems().removeAll(mniSaveNote, mniLoadNote);
                tabMenu.getItems().remove(mniLoadNote);
            }
        });
        tab1.setOnClosed((Event event) -> {
            System.out.println("Tab 1 wurde jetzt geschlossen");
        });
        tab1.setOnClosed((Event event) -> {
            System.out.println("Tab 2 wurde jetzt geschlossen");
        });
        tab1.setOnClosed((Event event) -> {
            System.out.println("Tab 3 wurde jetzt geschlossen");
        });
        tab1.setOnClosed((Event event) -> {
            System.out.println("Tab 4 wurde jetzt geschlossen");
        });

        return tabPane;
    }

    public void activateTab(int tabNr) {  // tabNr 1, 2, 3, 4
        tabPane.getSelectionModel().select(tabNr - 1);  // tab mit tabNr aktiv machen
    }

    public TextArea setupTextArea() {  // in tab4 verwendet
        TextArea textArea = new TextArea();
        textArea.setEditable(true);
        textArea.setWrapText(true);
        return textArea;
    }

    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(CONF_WINPOS_X, DEFAULT_WINPOS_X_TXT);
        primaryStage.setX(Double.parseDouble(winPosX_txt));
        String winPosY_txt = confProps.getProperty(CONF_WINPOS_Y, DEFAULT_WINPOS_Y_TXT);
        primaryStage.setY(Double.parseDouble(winPosY_txt));
    }

    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'%n", e.getMessage());
            }
        }
    }

    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(CONF_WINPOS_X, "" + primaryStage.getX());
        confProps.setProperty(CONF_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%n", //
                    CONF_FILE.getAbsolutePath(), e.getMessage());
        }
    }

    public void loadNote() {
        System.out.println("loadNote() called");
    }

    public Stage getPrimaryStage() {
        return primaryStage;
    }
}

9.2.2. Klasse 'my.pkg.duty.QuickNote'

package my.pkg.duty;

import java.io.File;
import java.io.IOException;
import java.util.List;

import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.scene.control.Alert;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextArea;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
import my.pkg.JfxApp04b;

public class QuickNote {

    private JfxApp04b app;
    private int tabId;
    private TabPane containingTabPane;
    private Menu containingMenu;
    private Tab myTab;
    private MenuItem mniSaveNote;
    private MenuItem mniLoadNote;
    private TextArea noteTextArea;

    public QuickNote(JfxApp04b app, int tabId, TabPane containingTabPane, Menu containingMenu) {
        this.app = app;
        this.tabId = tabId;
        this.containingTabPane = containingTabPane;
        this.containingMenu = containingMenu;
        this.mniSaveNote = new MenuItem();
        this.mniLoadNote = new MenuItem();
        this.noteTextArea = new TextArea();
        this.noteTextArea.setEditable(true);
        this.noteTextArea.setWrapText(true);
    }

    /**
     * Adds its MenuItems as soon as the tab gets active.
     *
     * @param containingMenu the Menu where the menuItems should be added
     */
    private void addMenuItems(String tabName) {
        assert (!containingMenu.getItems().contains(mniLoadNote))
                : "MenuItem 'mniLoadNote' already there";
        containingMenu.getItems().addAll(mniLoadNote, mniSaveNote);
        mniLoadNote.setText("'%s' Notiz laden".formatted(tabName));
        mniSaveNote.setText("'%s' Notiz speichern".formatted(tabName));
        mniLoadNote.setOnAction((ActionEvent event) -> {
            String txt = loadNoteFromFile();
            noteTextArea.setText(txt);
        });
    }

    /**
     * Removes its MenuItems as soon as tab gets inactive.
     *
     * @param containingMenu the Menu fron which the menuItems should be removed
     */
    private void removeMenuItems() {
        assert (containingMenu.getItems().contains(mniLoadNote))
                : "MenuItem 'mniLoadNote' is not there";
        containingMenu.getItems().removeAll(mniLoadNote, mniSaveNote);
    }

    public void setupMyTab(int tabIdx) {
        assert (myTab == null) : "myTab already instantiated";
        System.out.format("'setupMyTab(%d)' aktiv%n", tabIdx);
        myTab = new Tab();
        List<Tab> tabs = containingTabPane.getTabs();
        if (tabIdx < 0) {
            tabs.add(myTab);
            myTab.setText("Tab " + tabId);
        } else {
            //TODO: check if valid position
            tabs.set(tabIdx, myTab);
            // assuming no individual/semantic names are set:
            for (int i = tabIdx; i < tabs.size(); i++) {
                tabs.get(i).setText("" + (i + 1));
            }
        }
        myTab.setContent(noteTextArea);
        myTab.setOnSelectionChanged((Event event) -> {
            if (myTab.isSelected()) {
                System.out.format("'%s' wurde selektiert%n", myTab.getText());
                addMenuItems(myTab.getText());
            } else {
                System.out.format("'%s' wurde inaktiv%n", myTab.getText());
                removeMenuItems();
            }
        });
        myTab.setOnClosed((Event event) -> {
            System.out.format("'%s' wurde jetzt geschlossen%n", myTab.getText());
        });
    }

    public void removeMyTab() {
        assert (myTab != null) : "myTab is null";
        //TODO: check if it was active, then select another one, so that menu is removed, ...
        boolean done = containingTabPane.getTabs().remove(myTab);
        assert (done) : "myTab was not in tabs list";
    }

    public void saveNoteToFile() {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Notiz speichern");
        fileChooser.getExtensionFilters().addAll(new ExtensionFilter("Text Files", "*.txt"),
                new ExtensionFilter("All Files", "*.*"));
        File selectedFile = fileChooser.showOpenDialog(app.getPrimaryStage());
        if (selectedFile != null) {
            app.getEngine().saveNote(selectedFile, noteTextArea.getText());
        }
    }

    public String loadNoteFromFile() {
        String txt = "";
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Notiz laden");
        fileChooser.getExtensionFilters().addAll(new ExtensionFilter("Text Files", "*.txt"),
                new ExtensionFilter("All Files", "*.*"));
        File selectedFile = fileChooser.showOpenDialog(app.getPrimaryStage());
        if (selectedFile != null) {
            try {
                txt = app.getEngine().loadNote(selectedFile);
            } catch (IOException e) {
                Alert alert = new Alert(AlertType.ERROR);
                alert.setTitle("Laden einer Notiz fehlgeschlagen");
                alert.setHeaderText("Notiz %s nicht gelungen".formatted(selectedFile.getAbsolutePath()));
                alert.setContentText(e.getMessage());
                alert.initOwner(app.getPrimaryStage());
                alert.showAndWait();
            }
            //noteTextArea.setText(txt);
        }
        return txt;
    }
}

9.2.3. Klasse 'my.pkg.engine.Engine'

package my.pkg.engine;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class Engine {

    public Engine() {

    }

    public void saveNote(File file, String txt) {
        System.out.println("engine-saveNote called");

    }

    public String loadNote(File file) throws FileNotFoundException, IOException {
        System.out.println("engine-loadNote called");
        String txt = "";
        try (FileReader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);) {
            String line;
            int idx = 0;
            while ((line = br.readLine()) != null) {
                txt += line;
            }
        }
        return txt;
    }
}

9.2.4. Datei 'jfx-app-4.css'

.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;
}
/*
VBox {
    -fx-padding: 10 10 10 10;
    -fx-background: green;
}
*/
Tab  {
/*
    -fx-padding: 10 10 10 10;
    -fx-background: green;
*/
}
/* .tab-content-area > * */
.tab-content-area {
    -fx-padding: 10 10 10 10;
/*     -fx-background: green; */
}

10. Scribble

Links und Infos, die später in andere Dokumente wandern sollen …​

javafx - how to get java fx thread access - Stack Overflow …​ 2021-05-18 (e.g. from AWT or Swing window)

JavaFX Concurrency …​ 2021-05-18

https://www.eclipse.org/community/eclipse_newsletter/2017/october/article1.php [e4 on JavaFX | The Eclipse Foundation] …​ 2021-05-18

10.1. Web-Technologien

https://stackoverflow.com/questions/5378559/including-javascript-in-svg [Including JavaScript in SVG - Stack Overflow] …​ 18.5.2021, 21:11:15