Beispiel Jfx-App für GridPane und ListView

1. Überblick

Hier ein vollständiges Beispiel für GridPane und ListView mit MVC und gespeicherter App-Konfiguration (hier Fensterposition und Größe).

Es wird folgende Struktur für Java-Packages, Resource-Ordner, etc. verwendet:

JfxDemoListView1 logs (Log-Dateien) src/main/java:  my.jfxapp.demolistview1 config AppConf control GuiRootController exception  (SomeException, ...) model Engine Person util (GuiUtil, EngineUtil, ...) view GuiRoot  JfxApp src/main/resources:css application.cssicons htlw5-logo1.png app-conf.properties (auto-generated) logging.properties pom.xml (keine Besonderheiten)

Nachfolgend der vollständige Source-Code (aus/einklappbar), obige Links funktionieren !

2. Applikations-GUI und Hinweise

ScrShot20230521 JfxDemoListView1AppWin

Einige Hinweise:

Die App hat mit der Klasse AppConf die Möglichkeit, Konfigurationen bei Applikationsende automatisch zu speichern und beim Start wieder zu verwenden.
Dies ist hier für Fensterposition und Fenstergröße implementiert, verwendet in Klasse JfxApp mit Methode configWindow(…​) in Methode start(…​). Das Laden erfolgt mit einem "static initializer" beim Laden der Klasse AppConf ohne expliziten Aufruf.

WICHTIG:
damit die Settings gespeichert werden können, mus die App regulär beendet werden. Die Auto-Restart-Möglichkeit von IntelliJ tut dies nicht! Also mindestens einmal Fensterposition und -Größe passend setzen und App mit Schließknopf beenden, dann wird die Datei app-conf.properties mit passenden Werten erzeugt und ab nun genutzt.

Damit die Logging-Konfiguration, der logs-Ordner gefunden werden und app-conf.properties in Modul-Basisverzeichnis liegen, muss in der IntelliJ-Run-Configuration (erreichbar oben neben dem Run-Button) das Setting Add VM Options aktiviert werden und im nun vorhandenen Eingabefeld eingegeben werden:
"-Djava.util.logging.config.file=./logging.properties" (am besten davor noch -ea und Leerzeichen als Trenner)

Wichtige Konfigurationsinfos werden ganz zu Beginn im Terminal-Fenster ausgegeben (Assertions, Arbeitsverzeichnis und Logging-Konfiguration), sodass bei Bedarf Korrekturen leichter sind.

3. Kompletter Source-Code

3.1. Klasse '…​JfxApp'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.stage.Stage;
import my.jfxapp.demolistview1.config.AppConf;
import my.jfxapp.demolistview1.control.GuiRootController;
import my.jfxapp.demolistview1.model.Engine;
import my.jfxapp.demolistview1.view.GuiRoot;

import java.net.URL;
import java.util.logging.Logger;

public class JfxApp extends Application {
    private static final Class<?> CL = java.lang.invoke.MethodHandles.lookup().lookupClass();
    private static final String CLN = CL.getSimpleName();
    private static final Logger LOG = Logger.getLogger(CL.getName());

    // effectively final Singleton (only 1 instance) -> static var convenient:

    private GuiRootController guiRootController;

    //////// if access to appObj needed: ////////
    // private static JfxApp appObj = null;
    //
    // public static JfxApp obj() {
    //     return appObj;  // assigned best within init() - first instance method knowing 'this'
    // }
    // public GuiRoot getGuiRoot() {
    //     // primary stage and scene reachable from guiBase/root: root.getScene().getWindow()
    //     return guiRootController.getGuiRoot();
    // }
    /////////////////////////////////////////////

    public static void main(String[] args) {
        LOG.entering(CLN, "main", args);
        launch(args);
        LOG.exiting(CLN, "main");
    }

    @Override
    public void init() throws Exception {
        LOG.entering(CLN, "init");
        // if access to appObj needed:   JfxApp.appObj = this;
        super.init();
        guiRootController = new GuiRootController(new Engine(), new GuiRoot(), new AppConf());
        LOG.exiting(CLN, "init");
    }

    @Override
    public void stop() throws Exception {
        LOG.entering(CLN, "stop");
        AppConf.store(guiRootController);
        super.stop();
    }

    @Override
    public void start(Stage primStage) throws Exception {
        LOG.entering(CLN, "start", primStage);

        GuiRoot guiRoot = guiRootController.getGuiRoot();
        primStage.setScene(setupScene(guiRootController.getGuiRoot(), "/css/application.css"));
        guiRoot.initGui();
        guiRootController.smartifyGuiRoot();

        primStage.show();

        configWindow(primStage, "JfxApp Demo GridPane+ListView",
                "/icons/htlw5-logo1.png");

        LOG.exiting(CLN, "start");
    }

    public Scene setupScene(GuiRoot root, String cssRscPath) {
        Scene scene = new Scene(root);
        if (cssRscPath != null) {
            URL cssUrl = getClass().getResource(cssRscPath);  // "application.css"
            String cssLink = cssUrl.toExternalForm();  // gleichwertig: cssUrl.toString();
            scene.getStylesheets().add(cssLink);
            System.out.format("CSS-File URL is '%s'%n", cssLink);
        }
        return scene;
    }

    public void configWindow(Stage primStage, String appTitle, String appIconRscLoc) {

        // der Fenstertitel wird gesetzt (wie HTML-<title>):
        primStage.setTitle(appTitle);

        // Setting app icon:
        URL iconUrl = getClass().getResource(appIconRscLoc);
        String iconLink = iconUrl.toExternalForm();
        primStage.getIcons().add(new Image(iconLink));

        AppConf appConf = guiRootController.getAppConf();
        primStage.setX(appConf.getMainWinPosX());
        primStage.setY(appConf.getMainWinPosY());
        primStage.setWidth(appConf.getMainWinWidth());
        primStage.setHeight(appConf.getMainWinHeight());
    }
}

3.2. Klasse '…​view.GuiRoot'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1.view;

import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;

import java.util.logging.Logger;

public class GuiRoot extends BorderPane {  // or VBox, ....
    private static final Class<?> CL = java.lang.invoke.MethodHandles.lookup().lookupClass();
    private static final String CLN = CL.getSimpleName();
    private static final Logger LOG = Logger.getLogger(CL.getName());
    private TextField txfName;
    private ListView lsvPersonen;
    private TextField txfBirthDate;
    private TextArea txaInfo;
    private GridPane gpnPersonsDisplay;
    private Button btnToggleGrdLnVisible;
    private Label lblStatusInfo;

    public GuiRoot initGui() {
        LOG.entering(CLN, "initGui");
//        this.setLeft(setupLeft());
        gpnPersonsDisplay = new GridPane(); // needed in both center and bottom (Button GridLn on/off)
        this.setCenter(setupCenter());
        this.setBottom(setupBottom());
        LOG.exiting(CLN, "initGui");
        return this;
    }

    public HBox setupBottom() {
        lblStatusInfo = new Label("could be a status info, not implemented for now.\n\n"
                + "Comment out the stmt. mentioned below if no need\nto visualize gaps and cells:\n"
                + "    'gpnPersonForm.setGridLinesVisible(true);' ");

        btnToggleGrdLnVisible = new Button("Toggle\nGridLines\nVisible");

        HBox hbox = new HBox(20, lblStatusInfo, btnToggleGrdLnVisible);

        //Layout + Styling:
        lblStatusInfo.setId("lblStatusInfo");
        btnToggleGrdLnVisible.setId("btnToggleGrdLnVisible");

        return hbox;
    }

    public GridPane setupCenter() {
        int rowIdx = 0;
        // the ListView within col-0: rowspan one more than needed to show it is height 0
        lsvPersonen = new ListView();
        lsvPersonen.setPrefSize(250, 400);
        //TODO: ensure proper horizontal ListView scrollbar steps for buttons and non-slider areas

        gpnPersonsDisplay.add(lsvPersonen, 0, 0, 1, 4);

        txfName = new TextField();
        txfBirthDate = new TextField();
        txaInfo = new TextArea();

        gpnPersonsDisplay.add(new Label("Name"), 1, rowIdx);
        gpnPersonsDisplay.add(txfName, 2, rowIdx);
        rowIdx++;
        gpnPersonsDisplay.add(new Label("BirthDate"), 1, rowIdx);
        gpnPersonsDisplay.add(txfBirthDate, 2, rowIdx);
        rowIdx++;
        gpnPersonsDisplay.add(new Label("Info"), 1, rowIdx);
        gpnPersonsDisplay.add(txaInfo, 2, rowIdx);
        gpnPersonsDisplay.setPrefSize(300.0, 200.0);

        //Layout + Styling:
        lsvPersonen.setId("lsvPersonen");

        gpnPersonsDisplay.setGridLinesVisible(true);  // for devel. to see cells, gaps. Default: false
        //.. comment ^^ out if not needed to get overview! (last row has height 0 since not used)

        ColumnConstraints colConstraint0 = new ColumnConstraints(200, 400, 400);
        ColumnConstraints colConstraint1 = new ColumnConstraints(80, 100, 150);
        ColumnConstraints colConstraint2 = new ColumnConstraints(100, 200, 300);
        colConstraint1.setHgrow(Priority.ALWAYS);
        gpnPersonsDisplay.getColumnConstraints().addAll(colConstraint0, colConstraint1, colConstraint2);
        gpnPersonsDisplay.setHgap(8); //horizontal gap in pixels
        gpnPersonsDisplay.setVgap(8); //vertical gap in pixels
        gpnPersonsDisplay.setPadding(new Insets(8, 8, 8, 8)); //margins around whole gpn

        return gpnPersonsDisplay;
    }

    public ListView getLsvPersonen() {
        return lsvPersonen;
    }

    public TextField getTxfName() {
        return txfName;
    }

    public TextField getTxfBirthDate() {
        return txfBirthDate;
    }

    public TextArea getTxaInfo() {
        return txaInfo;
    }

    public GridPane getGpnPersonsDisplay() {
        return gpnPersonsDisplay;
    }

    public Button getBtnToggleGrdLnVisible() {
        return btnToggleGrdLnVisible;
    }
}

3.3. Klasse '…​control.GuiRootController'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1.control;

import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.scene.control.ListView;
import my.jfxapp.demolistview1.config.AppConf;
import my.jfxapp.demolistview1.model.Engine;
import my.jfxapp.demolistview1.view.GuiRoot;
import my.jfxapp.demolistview1.model.Person;

import java.util.logging.Logger;

public class GuiRootController {
    private static final Class<?> CL = java.lang.invoke.MethodHandles.lookup().lookupClass();
    private static final String CLN = CL.getSimpleName();
    private static final Logger LOG = Logger.getLogger(CL.getName());

    private GuiRoot guiRoot;
    private Engine engine;
    private AppConf appConf;

    public GuiRootController(Engine engine, GuiRoot guiRoot, AppConf appConf) {
        this.engine = engine;
        this.guiRoot = guiRoot;
        this.appConf = appConf;
    }

    public GuiRoot getGuiRoot() {
        return guiRoot;
    }

    public void smartifyGuiRoot() {
        ListView<Person> lsvPers = guiRoot.getLsvPersonen();  //
        lsvPers.setItems(FXCollections.observableList(engine.getPersonen()));

        lsvPers.getSelectionModel().selectedItemProperty().addListener(
                (ChangeListener<Person>) (observable, oldPerson, newPerson) -> {
                    System.out.println("ListView selection changed from oldPerson = "
                            + oldPerson + " to newPerson = " + newPerson);
                    updatePersonDetailsForm(newPerson);
                });

        guiRoot.getBtnToggleGrdLnVisible().setOnAction(event -> {
            boolean newState = ! guiRoot.getGpnPersonsDisplay().gridLinesVisibleProperty().get();
            guiRoot.getGpnPersonsDisplay().setGridLinesVisible(newState);
        });

        // Has to be done AFTER selectedItemProperty().addListener(..) to actually show
        //.. the selected item within the person details form:
        LOG.info(String.format("Focused Idx before selection: %d",
                lsvPers.getFocusModel().getFocusedIndex()));
        lsvPers.getSelectionModel().selectRange(4, 5); // Par1: incl, par2: excl.!

        LOG.info(String.format("Focused Idx after selection: %d",
                lsvPers.getFocusModel().getFocusedIndex()));

        lsvPers.getFocusModel().focus(1);  // normally same as selected!
        LOG.info(String.format("Focused Idx after setting focus explicitely: %d",
                lsvPers.getFocusModel().getFocusedIndex()));

        // Focus is important if selected range > 1 !

        lsvPers.scrollTo(0);
    }

    public void updatePersonDetailsForm(Person person) {
        String name = person == null ? "" : person.getName();
        guiRoot.getTxfName().setText(name);
        String birthDtTxt = person == null ? "" : person.getBirthDate().toString();
        guiRoot.getTxfBirthDate().setText(birthDtTxt);
        String info = person == null ? "" : person.getInfo();
        guiRoot.getTxaInfo().setText(info);
    }

    public AppConf getAppConf() {
        return appConf;
    }
}

3.4. Klasse '…​model.Engine'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1.model;

import my.jfxapp.demolistview1.model.Person;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class Engine {

    private List<Person> personen = new ArrayList<>(Arrays.asList(Person.demoPersArr()));

    public List<Person> getPersonen() {
        return Collections.unmodifiableList(personen);
    }
}

3.5. Klasse '…​model.Person'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1.model;

import java.time.LocalDate;
import java.util.Objects;

public class Person {
    private String name;
    private LocalDate birthDate;
    private String info;

    public Person(String name, LocalDate birthDate, String info) {
        this.name = name;
        this.birthDate = birthDate;
        this.info = info;
    }

    public static Person[] demoPersArr() {
        Person[] demoPersons = {
                new Person("Fritzi", LocalDate.of(1999,12,31), "keine Info"),
                new Person("Evi", LocalDate.of(2004,1, 1), "unbekannt"),
                new Person("Ada", LocalDate.of(2004,7, 7), "nichts"),
                new Person("Ute", LocalDate.of(1951,2, 28), "folgt später"),
                new Person("Ida", LocalDate.of(1955,5, 9), "diverses"),
                new Person("Edi", LocalDate.of(1968,2, 29), "geheim"),
                new Person("Udo", LocalDate.of(1955,11, 9), "leer"),
        };
        return demoPersons;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Person person)) return false;
        return Objects.equals(name, person.name) && Objects.equals(birthDate, person.birthDate);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, birthDate);
    }

    @Override
    public String toString() {
        return "name='" + name + '\'' + ", gebDate=" + birthDate + ", info='" + info + '\'';
    }

    public String getName() {
        return name;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }

    public String getInfo() {
        return info;
    }
}

3.6. Klasse '…​config.AppConf'

Source-Code (auf-/einklappbar)
package my.jfxapp.demolistview1.config;

import javafx.geometry.Rectangle2D;
import javafx.stage.Screen;
import javafx.stage.Stage;
import my.jfxapp.demolistview1.control.GuiRootController;

import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Locale;
import java.util.Properties;
import java.util.logging.Logger;

public class AppConf {

    private static final Class<?> CL = java.lang.invoke.MethodHandles.lookup().lookupClass();
    private static final String CLN = CL.getSimpleName();
    private static final Logger LOG = Logger.getLogger(CL.getName());

    private static final File CONF_FILE = new File("./app-conf.properties"); // in project basedir
    public static final String KEY_MAINWIN_POSX = "mainwin.posx";
    public static final String KEY_MAINWIN_POSY = "mainwin.posy";
    public static final String KEY_MAINWIN_WIDTH = "mainwin.width";
    public static final String KEY_MAINWIN_HEIGHT = "mainwin.height";

    public static final String DEFAULT_MAINWIN_POSX_TXT = "0.0";
    public static final String DEFAULT_MAINWIN_POSY_TXT = "100.0";
    public static final String DEFAULT_MAINWIN_WIDTH_TXT = "500.0";
    public static final String DEFAULT_MAINWIN_HEIGHT_TXT = "300.0";

    private static final Properties PROPS = new Properties();
    public static final String PROP_JUL_CONF_FILE = "java.util.logging.config.file";

    static {  // "static initializer" - running only once as soon as CLASS (not Object) is loaded

        // Check if 'assert ...' statements are enabled or not:
        boolean assertsEnabled = false;  // will remain in this state if no assertions
        assert assertsEnabled = true; // if running because they are enabled, sets and returns true
        String assertsInfo = String.format("%n    !! ASSERTIONS %s enabled !!%n        %s%n",
                (assertsEnabled ? "ARE" : "NOT"), "enable by VM-Option '-ea'");

        // Current Working Dir (should be $MODULE_DIR$ in IntelliJ during development):
        File cwd = new File("");
        // or: File cwd = System.getProperty("user.dir");
        String cwdInfo = String.format("%n    Current WorkDir ('cwd', should be $MODULE_DIR$"
                + " in IntelliJ during development):%n        '%s'%n", cwd.getAbsolutePath());

        //Logging:
        boolean loggingUp;
        String loggingInfo;
        Locale.setDefault(Locale.ROOT);
        String julConfPropsPath = System.getProperty(PROP_JUL_CONF_FILE);
        if (julConfPropsPath == null) {
            loggingUp = true;
            loggingInfo = String.format("%n    SysProp '%s' not set, using"
                    + " standard logging settings%n        set by "
                            + "VM-Option - e.g. \"-D%1$s=./logging.properties\" in cwd (%s)%n",
                    PROP_JUL_CONF_FILE, cwd.getAbsolutePath());
        } else {
            File julConfPropsFile = new File(julConfPropsPath);
            if (!julConfPropsFile.isFile()) {
                loggingUp = false;
                loggingInfo = String.format("%n    '%s' defined by SysProp '%s' does not exist"
                                + " or not a file:"
                                + "%n        '%s'%n        -> NO LOGGING at all!%n",
                        julConfPropsPath, PROP_JUL_CONF_FILE, julConfPropsFile.getAbsolutePath());
            } else if (!julConfPropsFile.canRead()) {
                loggingUp = false;
                loggingInfo = String.format("%n    '%s' defined by SysProp '%s' cannot be read:"
                                + "%n        '%s'%n        -> NO LOGGING at all!%n",
                        julConfPropsPath, PROP_JUL_CONF_FILE, julConfPropsFile.getAbsolutePath());
            } else {
                try {
                    loggingUp = true;
                    loggingInfo = String.format("%n    Logging config file '%s' used!%n",
                            julConfPropsFile.getCanonicalPath());
                } catch (IOException e) {
                    loggingUp = false;
                    loggingInfo = String.format("%n    Problem with logConfFile '%s'%n"
                                    + "        ('%s')%n         -> NO LOGGING:  %s%n",
                            julConfPropsPath, julConfPropsFile.getAbsolutePath(), e.getMessage());
                }
            }
        }
        System.out.println("~~~~ You should see Infos about assert, logging, workdir etc now"
                + " (as first log messages or before '~~~~~~~~~~~~~~~~')");
        if (loggingUp) {
            LOG.info(assertsInfo);
            LOG.info(cwdInfo);
            LOG.info(loggingInfo);
        } else {
            System.out.println(assertsInfo);
            System.out.println(cwdInfo);
            System.out.println(loggingInfo);
        }
        System.out.println("~~~~~~~~~~~~~~~~");
    }

    static {  // it is possible to have many static initializers
        if (CONF_FILE.exists()) {
            try (FileReader fr = new FileReader(CONF_FILE)) {
                PROPS.load(fr);
            } catch (IOException e) {
                System.out.format("Problem reading config file: %s", e.getMessage());
            }
        }
    }

    /**
     * Method to store all the wanted settings. <br>
     * Should be called at app exit - best within 'stop()'. <br>
     * Could store more settings - e.g. current textfields content, etc. <br>
     * <p>The PROPS and all the KEY_* are public, accessable everywhere by AppConf.PROPS etc.</p>
     */
    public static void store(GuiRootController guiRootController) {
        LOG.entering(CLN, "store");
        assert (PROPS != null) : "no confProps set";  // compact way to check preconditions
        //
        // Every node knows its parent and its scene, scene knows its window (parent of stage):
        Stage primStage = (Stage) guiRootController.getGuiRoot().getScene().getWindow();
        //
        // collecting all current settings within confProps:
        PROPS.setProperty(KEY_MAINWIN_POSX, "" + primStage.getX());
        PROPS.setProperty(KEY_MAINWIN_POSY, "" + primStage.getY());
        PROPS.setProperty(KEY_MAINWIN_WIDTH, "" + primStage.getWidth());
        PROPS.setProperty(KEY_MAINWIN_HEIGHT, "" + primStage.getHeight());
        //
        try (FileWriter fw = new FileWriter(CONF_FILE)) {
            PROPS.store(fw, "JfxApp Config");
        } catch (IOException e) {
            System.out.format("Problem beim Schreiben von %s: %s", //
                    CONF_FILE.getAbsolutePath(), e.getMessage());
        }
    }

    private static double doubleValue(String propKey, String propDefaultVal, Double minLimit,
            Double maxLimit) {
        String txtVal = PROPS.getProperty(propKey, propDefaultVal);
        double dblVal = Double.parseDouble(txtVal);
        if (minLimit != null && maxLimit != null && minLimit > maxLimit) {
            throw new IllegalArgumentException(String.format("for %s: minLimit %1.3f > maxLimit "
                            + "%1.3f",
                    propKey, minLimit, maxLimit));
        } else if (minLimit != 0 && dblVal < minLimit) {
            dblVal = minLimit;
        } else if (maxLimit != null && dblVal > maxLimit) {
            dblVal = maxLimit;
        }
        LOG.fine(String.format("Final value for %s: %7.2f", propKey, dblVal));
        return dblVal;
    }

    public static double getMainWinPosX() {
        Rectangle2D primScreenGeom = Screen.getPrimary().getBounds();
        return doubleValue(KEY_MAINWIN_POSX, DEFAULT_MAINWIN_POSX_TXT,
                primScreenGeom.getMinX(), primScreenGeom.getMaxX() - 100);
    }

    public static double getMainWinPosY() {
        Rectangle2D primScreenGeom = Screen.getPrimary().getBounds();
        return doubleValue(KEY_MAINWIN_POSY, DEFAULT_MAINWIN_POSY_TXT,
                primScreenGeom.getMinY(), primScreenGeom.getMaxY() - 100);
    }

    public static double getMainWinWidth() {
        Rectangle2D primScreenGeom = Screen.getPrimary().getBounds();
        return doubleValue(KEY_MAINWIN_WIDTH, DEFAULT_MAINWIN_WIDTH_TXT,
                100.0, primScreenGeom.getMaxX() - primScreenGeom.getMinX());
    }

    public static double getMainWinHeight() {
        Rectangle2D primScreenGeom = Screen.getPrimary().getBounds();
        return doubleValue(KEY_MAINWIN_HEIGHT, DEFAULT_MAINWIN_HEIGHT_TXT,
                100.0, primScreenGeom.getMaxY() - primScreenGeom.getMinY());
    }
}

3.7. StyleSheet '/css/application.css'

Source-Code (auf-/einklappbar)
.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(255, 255, 191);
}

#lsvPersonen {
    -fx-border-color: red;
    -fx-border-width: 0.2em;
}

#btnToggleGrdLnVisible {
    -fx-background-color: lightgreen;
    -fx-border-radius: 0.8em;
    -fx-border-color: red; -fx-border-width: 0.2em;
}

#lblStatusInfo {
    -fx-font-size: 0.7em;
}

.lblHelloClass {  /* not used */
    -fx-font-weight: bold;
}

3.8. AppIcon '/icons/htlw5-logo1.png'

Hier das AppIcon:         htlw5-logo1.png
(direkt herunterladen im Browser per Rechtsklick auf das Icon, "Bild speichern" o.ä.)

3.9. Log-Konfiguration 'logging.properties'

Source-Code (auf-/einklappbar)
####### Logging Konfiguration JfxLayoutsDemo ########
# zur Nutzung: bei Programm-Aufruf VM-Arg.: java -Djava.util.logging.config.file=./logging.properties
#
### Am besten als erste Elemente nach 'public class Xxx {...':
# private static final Class<?> CL = java.lang.invoke.MethodHandles.lookup().lookupClass();
# private static final String CLN = CL.getSimpleName();
# private static final Logger LOG = Logger.getLogger(CL.getName());
# Am Methoden-Anfang: LOG.entering(CLN, "methodName(Argyp1, Argtyp2, ...), arg1, arg2, ...");
# bei Exceptions: MyException ex = new MyException("die Msg");
#                 LOG.log(Level.SEVERE, ex.getMessage(), ex);
#                 throw ex;
# im Normalfall:  LOG.info("die Msg");
# Am Methoden-Ende:   LOG.exiting(CLN, "methodName(...)");

# Raute am Zeilenanfang: Kommentarzeile!!

#handlers= java.util.logging.ConsoleHandler
handlers = java.util.logging.FileHandler, java.util.logging.ConsoleHandler
.level= INFO

my.level = FINE
my.jfxapp.layoutsdemo.level = FINER

#### Formatierung (siehe Javadoc zu 'java.util.Formatter'): #####
### %1$: Zeitpunkt, %2$: Source (Aufrufer, sonst Logger-Name), %3$: Logger-Name (selten noetig),
### %4$: Log Level, %5$: Log-Text, %6$: Stacktrace, wenn da
java.util.logging.SimpleFormatter.format = %1$tF,%1$tT.%1$tL %4$7s: [%2$s] %5$s %6$s %n
# oder: java.util.logging.SimpleFormatter.format = --> %1$tF,%1$tT.%1tL %4$7s: [%2$s] %5$s %6$s !RMx!%n

##### Handler-spezifische Properties: #####

# ConsoleHandler benoetigt zusaetzliche Details-Obergrenze (am besten ALL):
java.util.logging.ConsoleHandler.level = ALL
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter

# Die Nachrichten in eine Datei mit angegebenem Pfad (mit 2 Platzhaltern) schreiben:
java.util.logging.FileHandler.pattern = ./logs/RxJfxSimpleApp_g%g-u%u.log
java.util.logging.FileHandler.limit = 250000
java.util.logging.FileHandler.count = 10
java.util.logging.FileHandler.formatter = java.util.logging.SimpleFormatter
## "/" the local pathname separator
## "%t" the system temporary directory
## "%h" the value of the "user.home" system property
## "%g" the generation number to distinguish rotated logs
## "%u" a unique number to resolve conflicts
## "%%" translates to a single percent sign "%"
### END ###