FXML, Layouts und CSS: Das Hauptfenster der Desktop-App gestalten

https://bildung.social/@oerinformatik/

https://oer-informatik.de/javafx-fxml-layouts-css

JavaFX, FXML, FXMLLoader, Controller, fx:id, Scene Builder, BorderPane, GridPane, Layout-Container, JavaFX-CSS, Kanban

tl/dr; (ca. 28 min Lesezeit): Das zweite Ticket der Desktop-App: Der Aufbau des Hauptfensters wandert aus dem Java-Code in eine FXML-Datei. Du ordnest die Oberfläche mit den Layout-Containern BorderPane, HBox, VBox und GridPane, verbindest sie über fx:id, @FXML und onAction mit einem Controller, lernst den Scene Builder als grafischen Editor kennen und gestaltest das Fenster mit JavaFX-CSS. Zum Schluss geht es um die typischen Fehlermeldungen beim Laden von FXML [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über eine Desktop-App mit JavaFX: Übersicht / Kanban-Projektstart / Projekt anlegen / FXML, Layouts und CSS / Properties, Bindings und MVVM / Backend anbinden / TableView für Buchungen / Formulare und Dialoge / Anmeldung mit Keycloak / Verwaltung und Diagramme / GUI-Tests mit TestFX / Abnahmetest und Kanban-Metriken / Paketieren mit jpackage

Schon fit in diesem Thema? Nutze die Kann-Liste und die Leitfragen unter dem Artikel als Checkliste, ob Du die relevanten Kompetenzen bereits hast.

Dieser Artikel baut auf der Kann-Liste des Artikels JavaFX-Projekt anlegen: Lebenszyklus, Stage, Scene und Node auf.

Als JavaFX-Grundlagen kann ich nach Durchlesen des Artikels…

  • … erklären, wozu FXML dient, und eine FXML-Datei mit Importen, Wurzelelement und verschachtelten Nodes lesen und schreiben.

  • … die Layout-Container BorderPane, HBox, VBox und GridPane unterscheiden und für eine gegebene Anordnung den passenden Container wählen.

  • … eine FXML-Datei über fx:controller, fx:id, @FXML und onAction="#methode" mit einer Controller-Klasse verbinden und die Methode initialize() nutzen.

  • … eine FXML-Datei mit FXMLLoader laden und als Wurzelknoten einer Scene verwenden.

  • … ein Stylesheet an eine Scene hängen und Nodes mit JavaFX-CSS (-fx-…-Eigenschaften, Style-Klassen, IDs) gestalten.

  • … den Scene Builder einordnen und sagen, was er an der FXML-Datei ändert.

Darüber hinaus kann ich als JavaFX-Expertenwissen…

  • … Fehlermeldungen beim Laden von FXML (LoadException, ClassNotFoundException, InaccessibleObjectException, NullPointerException in einem @FXML-Feld) einer Ursache zuordnen und beheben.

  • … begründen, warum ein Fehler im Modulsystem (opens … to javafx.fxml) in den Tests nicht auffällt, und das im Ablauf des Teams berücksichtigen.

Nach dem ersten Ticket startet die Desktop-App von Mitfahrlabor, zeigt aber nur zwei Labels. Mit dem zweiten Ticket bekommt das Hauptfenster eine Struktur: oben eine Kopfzeile, links die Liste „Meine Fahrtangebote“, rechts daneben die Details des gewählten Angebots, unten eine Statuszeile. Dabei ändert sich auch, wo die Oberfläche beschrieben wird. Statt Nodes im Java-Code mit new zu erzeugen, steht der Aufbau in einer FXML-Datei, das Aussehen in einem Stylesheet, und der Java-Code kümmert sich nur noch um das Verhalten.

Stand auf dem Board

Im Kanban-Meeting war „In Arbeit“ wieder frei, und Du hast JFX-02 „Hauptfenster mit FXML und CSS“ aus „Bereit“ gezogen. Die Akzeptanzkriterien:

  • Angenommen, die App startet, dann zeigt das Hauptfenster links die Liste „Meine Fahrtangebote“ und rechts die Details des gewählten Angebots.
  • Die Oberfläche ist in FXML beschrieben und mit einer CSS-Datei gestaltet; Bedienelemente sind mit der Tastatur erreichbar.
JFX-02 auf dem Board
vor diesem Artikel nach diesem Artikel
JFX-01 Projekt anlegen Fertig Fertig
JFX-02 Hauptfenster mit FXML und CSS In Arbeit – läuft Fertig
Bereit (WIP 3) JFX-03, JFX-04 JFX-03 wird als Nächstes gezogen
Daten in der Liste keine zwei Angebote als lokale Beispieldaten
Code-Stand src/02-projekt-anlegen/mitfahrlabor-desktop src/03-fxml-layouts-css/mitfahrlabor-desktop

Echte Daten aus dem Backend gehören nicht zu diesem Ticket, sondern zu JFX-04. Damit die Liste trotzdem etwas zeigt, nutzt der Code zwei Fahrtangebote von Ben Richter aus den Beispieldaten des Backends (data.sql): beide am 19.10.2026 ab „Hauptbahnhof, ZOB Steig 3“ in Gütersloh, einmal um 07:00 Uhr zur Schule, einmal um 14:20 Uhr nach Hause. Das Ticket bleibt so klein, wie es im Replenishment geschätzt wurde.

FXML trennt Aufbau und Verhalten

FXML ist eine XML-Sprache, in der Du den Scene Graph einer JavaFX-Oberfläche beschreibst. Jedes XML-Element steht für ein Objekt (meist einen Node), jedes Attribut für eine Eigenschaft. Aus

<Label fx:id="ueberschrift" text="Meine Fahrtangebote" styleClass="bereich-titel"/>

macht JavaFX beim Laden dasselbe, was Du im letzten Artikel von Hand geschrieben hast: new Label(), dann setText("Meine Fahrtangebote") und getStyleClass().add("bereich-titel").

Das bringt drei Vorteile:

  • Trennung: Der Aufbau der Oberfläche (FXML), das Aussehen (CSS) und das Verhalten (Java-Controller) stehen in getrennten Dateien. Das kennst Du aus dem Web mit HTML, CSS und JavaScript.
  • Lesbarkeit: Die Verschachtelung der XML-Elemente zeigt den Baum der Nodes direkt. Im Java-Code musst Du ihn aus getChildren().add(…)-Aufrufen zusammensuchen.
  • Werkzeuge: FXML-Dateien lassen sich mit dem Scene Builder grafisch bearbeiten, ohne Java zu ändern.

Die Alternative bleibt erlaubt: Oberflächen, die zur Laufzeit stark wechseln (etwa eine Zeile je Datensatz), baut man weiter im Java-Code. Auch eine FXML-Oberfläche lässt sich danach im Code ergänzen.

Das Modul javafx.fxml einbinden

FXML gehört nicht zu javafx.controls, sondern steckt im eigenen Modul javafx.fxml. Die Build-Datei braucht deshalb ein weiteres Modul bzw. eine weitere Abhängigkeit:

In build.gradle.kts kommt das Modul in die Liste des JavaFX-Plugins:

javafx {
    version = "27.0.1"
    modules = listOf("javafx.controls", "javafx.fxml")
}

Auch die module-info.java ändert sich an zwei Stellen:

module de.dbh.mitfahrlabor.desktop {
    requires javafx.controls;
    requires javafx.fxml;

    exports de.dbh.mitfahrlabor.desktop;
    // FXMLLoader setzt @FXML-Felder per Reflexion: Paket für javafx.fxml öffnen
    opens de.dbh.mitfahrlabor.desktop to javafx.fxml;
}
  • requires javafx.fxml; macht FXMLLoader und die Annotation @FXML für Deinen Code sichtbar.
  • opens … to javafx.fxml; erlaubt dem Modul javafx.fxml, per Reflexion auf die privaten Felder und Methoden Deines Controllers zuzugreifen. exports reicht dafür nicht: Es gibt nur die öffentlichen Typen frei. Ohne opens scheitert der Start mit einer InaccessibleObjectException (mehr im Abschnitt „Typische Fehler mit FXML“).

Layout-Container: BorderPane, HBox, VBox und GridPane

Bevor die FXML-Datei entsteht, lohnt ein Blick auf die Werkzeuge für die Anordnung. Ein Layout-Container (in JavaFX eine Unterklasse von Pane) ist ein Parent-Node, der seine Kinder nach einer festen Regel platziert und bei Größenänderungen des Fensters neu anordnet. Du gibst also keine Pixelkoordinaten vor, sondern wählst die Regel. Die vier wichtigsten:

Die vier wichtigsten Layout-Container
Container Regel typischer Einsatz wichtige Einstellungen
BorderPane fünf Bereiche: top, bottom, left, right, center; center bekommt den restlichen Platz Grundgerüst eines Fensters leere Bereiche brauchen keinen Platz; BorderPane.margin
HBox Kinder nebeneinander in einer Zeile Kopfzeile, Button-Leiste, Eingabe mit Einheit spacing, alignment, HBox.hgrow am Kind
VBox Kinder untereinander in einer Spalte Liste mit Überschrift, Menü-Spalte spacing, alignment, VBox.vgrow am Kind
GridPane Raster aus Zeilen und Spalten Formulare, Beschriftung und Wert nebeneinander GridPane.rowIndex/columnIndex, columnSpan, hgap, vgap, ColumnConstraints

Weitere Container gibt es für Sonderfälle: StackPane legt Kinder übereinander (z. B. ein Hinweis über einer Liste), FlowPane bricht Kinder wie Text in die nächste Zeile um, AnchorPane verankert Kinder an den Rändern. Für ein Hauptfenster reichen die vier aus der Tabelle fast immer, weil sie sich verschachteln lassen: Ein Container ist selbst ein Node und kann Kind eines anderen Containers sein.

Das Hauptfenster von JFX-02 ist genau so eine Verschachtelung. Das Grundgerüst ist eine BorderPane; ihre Bereiche enthalten wieder Container:

Schema der Layout-Container des Hauptfensters: BorderPane als Wurzelknoten; top eine HBox mit zwei Labels, einer wachsenden Region und dem Button Aktualisieren; left eine VBox mit Überschrift und ListView; center ein GridPane mit Beschriftungen in Spalte 0 und Werten in Spalte 1; right leer; bottom ein Label als Statuszeile (mit KI erstellt)

Drei Einstellungen sorgen dafür, dass sich das Fenster beim Vergrößern sinnvoll verhält:

  • Wachsen in der HBox: Eine leere Region mit HBox.hgrow="ALWAYS" nimmt den freien Platz in der Kopfzeile ein und schiebt den Button an den rechten Rand.
  • Wachsen in der VBox: VBox.vgrow="ALWAYS" an der ListView lässt die Liste mit dem Fenster in die Höhe wachsen, die Überschrift bleibt gleich hoch.
  • Spalten im GridPane: ColumnConstraints legen für Spalte 0 eine Mindestbreite fest und lassen Spalte 1 wachsen; so stehen alle Werte bündig untereinander.

Die Angaben HBox.hgrow, VBox.vgrow oder GridPane.rowIndex heißen statische Eigenschaften (Layout-Constraints): Sie stehen am Kind, gehören aber zum Container, der es anordnet. Im Java-Code schreibst Du sie als HBox.setHgrow(region, Priority.ALWAYS) oder GridPane.setRowIndex(label, 2).

Die FXML-Datei des Hauptfensters

FXML-Dateien liegen als Ressource neben den Klassen, die sie laden, aber im Ordner src/main/resources mit derselben Paketstruktur:

src/main/
├── java/de/dbh/mitfahrlabor/desktop/
│   ├── MitfahrlaborDesktopApp.java
│   ├── HauptfensterController.java
│   ├── Angebot.java
│   └── Beispieldaten.java
└── resources/de/dbh/mitfahrlabor/desktop/
    ├── hauptfenster.fxml
    └── mitfahrlabor.css

Die Datei hauptfenster.fxml im Überblick (die Zeilen 3 bis 6 der Details sind gekürzt, sie folgen demselben Muster):

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

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.VBox?>

<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
            fx:controller="de.dbh.mitfahrlabor.desktop.HauptfensterController"
            styleClass="hauptfenster">

    <!-- oben: Kopfzeile mit Titel und Button -->
    <top>
        <HBox styleClass="kopfzeile" spacing="12" alignment="CENTER_LEFT">
            <Label text="Mitfahrlabor" styleClass="app-titel"/>
            <Label text="Fahrer*innen-Sicht · Ben Richter"/>
            <Region HBox.hgrow="ALWAYS"/>
            <Button fx:id="aktualisierenButton" text="_Aktualisieren"
                    mnemonicParsing="true" onAction="#aktualisieren"/>
        </HBox>
    </top>

    <!-- links: Liste der eigenen Angebote -->
    <left>
        <VBox styleClass="bereich" spacing="8" prefWidth="280">
            <Label fx:id="ueberschrift" text="Meine Fahrtangebote" styleClass="bereich-titel"/>
            <ListView fx:id="angebotsliste" VBox.vgrow="ALWAYS"/>
        </VBox>
    </left>

    <!-- Mitte: Details des gewählten Angebots -->
    <center>
        <GridPane fx:id="details" styleClass="bereich" hgap="12" vgap="8">
            <columnConstraints>
                <ColumnConstraints minWidth="110"/>
                <ColumnConstraints hgrow="ALWAYS"/>
            </columnConstraints>
            <Label text="Details" styleClass="bereich-titel"
                   GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2"/>
            <Label text="Datum" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
            <Label fx:id="detailDatum" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
            <Label text="Abfahrt" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
            <Label fx:id="detailAbfahrt" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
            <!-- ... Richtung, Haltepunkt, Freie Plätze, Kostenbeitrag in den Zeilen 3 bis 6 ... -->
        </GridPane>
    </center>

    <!-- unten: Statuszeile -->
    <bottom>
        <Label fx:id="statuszeile" styleClass="statuszeile" maxWidth="Infinity">
            <BorderPane.margin>
                <Insets top="4"/>
            </BorderPane.margin>
        </Label>
    </bottom>
</BorderPane>

Die Bausteine der Datei:

  • Importe: <?import …?> entspricht dem import in Java. Jede Klasse, die als Element vorkommt, muss importiert sein. Erlaubt ist auch <?import javafx.scene.control.*?>, einzelne Importe zeigen aber auf einen Blick, was die Datei nutzt.
  • Wurzelelement: Genau ein Element umschließt alles, hier die BorderPane. Es trägt die beiden Namensräume: xmlns für die JavaFX-Klassen und xmlns:fx für die Sonderattribute mit dem Präfix fx:.
  • Attribute setzen Eigenschaften mit einfachen Werten: text, spacing, prefWidth, alignment="CENTER_LEFT" (ein Wert der Aufzählung Pos). maxWidth="Infinity" lässt die Statuszeile über die ganze Breite reichen.
  • Eigenschaftselemente in Kleinschreibung (<top>, <left>, <center>, <columnConstraints>, <BorderPane.margin>) setzen Eigenschaften, deren Wert selbst ein Objekt ist. Kinder ohne Eigenschaftselement landen in der Standard-Eigenschaft, bei HBox, VBox und GridPane in children.
  • fx:id gibt einem Node eine Kennung. Sie wird zugleich zur CSS-ID (#angebotsliste) und verbindet den Node mit einem gleichnamigen Feld im Controller.
  • fx:controller am Wurzelelement nennt die Controller-Klasse mit vollem Paketnamen.
  • onAction="#aktualisieren" verweist auf eine Methode des Controllers, die bei einem Klick (oder Enter, Leertaste, Tastenkürzel) aufgerufen wird. Das # unterscheidet den Methodenverweis von einem Skript.

Beim Lesen hilft der Vergleich mit dem Schema oben: Jede Einrückungsebene der FXML-Datei ist eine Ebene im Scene Graph.

Der Controller: fx:id, @FXML und onAction

Der Controller ist eine gewöhnliche Java-Klasse ohne besondere Oberklasse. Er bekommt Zugriff auf die Nodes, die er braucht, und enthält die Methoden für die Ereignisse:

public class HauptfensterController {

    // fx:id in der FXML-Datei = Name des Felds
    @FXML private ListView<Angebot> angebotsliste;
    @FXML private Label detailDatum;
    @FXML private Label detailAbfahrt;
    @FXML private Label detailRichtung;
    @FXML private Label detailHaltepunkt;
    @FXML private Label detailPlaetze;
    @FXML private Label detailKosten;
    @FXML private Label statuszeile;

    /** Ruft der FXMLLoader auf, nachdem alle @FXML-Felder gesetzt sind. */
    @FXML
    private void initialize() {
        angebotsliste.getSelectionModel().selectedItemProperty()
                .addListener((beobachtet, alt, neu) -> zeigeDetails(neu));
        ladeAngebote();
    }

    /** onAction="#aktualisieren" am Button in der FXML-Datei. */
    @FXML
    private void aktualisieren(ActionEvent ereignis) {
        ladeAngebote();
    }

    private void ladeAngebote() {
        angebotsliste.getItems().setAll(Beispieldaten.angeboteVonBen());
        statuszeile.setText(angebotsliste.getItems().size() + " Fahrtangebote (Beispieldaten)");
        zeigeDetails(null);
    }

    private void zeigeDetails(Angebot angebot) {
        if (angebot == null) {
            detailDatum.setText("–");
            // ... übrige Werte ebenso leeren ...
            detailHaltepunkt.setText("Bitte links ein Angebot wählen.");
            return;
        }
        detailDatum.setText(angebot.datumText());
        detailAbfahrt.setText(angebot.abfahrt() + " Uhr");
        // ... Richtung, Haltepunkt, Plätze und Kostenbeitrag ebenso ...
    }
}

Die drei Verbindungen zwischen FXML und Java:

So hängen FXML und Controller zusammen
in der FXML-Datei im Controller Regel
fx:controller="de.dbh.mitfahrlabor.desktop.HauptfensterController" die Klasse selbst voller Klassenname; die Klasse braucht einen parameterlosen Konstruktor
fx:id="angebotsliste" @FXML private ListView<Angebot> angebotsliste; Feldname = fx:id, Typ passend zum Element
onAction="#aktualisieren" @FXML private void aktualisieren(ActionEvent ereignis) Methodenname ohne #; Parameter ActionEvent darf fehlen

Die Annotation @FXML markiert Felder und Methoden, die der FXMLLoader setzen bzw. aufrufen darf, obwohl sie private sind. Öffentliche Felder würden ohne Annotation funktionieren, aber dann könnte jede andere Klasse die Nodes verändern. Mit private und @FXML bleibt der Zugriff auf FXMLLoader und Controller beschränkt.

Die Methode initialize() ersetzt für den Controller den Konstruktor: Im Konstruktor sind die @FXML-Felder noch null, in initialize() sind sie alle gesetzt. Hier füllt der Controller die Liste und meldet sich für Änderungen der Auswahl an. Die Zeile mit selectedItemProperty().addListener(…) bedeutet: „Wenn sich das gewählte Angebot ändert, rufe zeigeDetails mit dem neuen Angebot auf.“ Wie solche Properties und Listener genau funktionieren und wie man sie mit Bindings ersetzt, zeigt der nächste Artikel.

Die Liste enthält Objekte vom Typ Angebot, einem Record mit den Feldnamen aus dem Schema Angebot der OpenAPI-Beschreibung des Backends (datum, abfahrt, richtung, freiePlaetze, kostenbeitragEur). Statt der haltepunktId hält er vorerst den Namen des Haltepunkts. Eine ListView zeigt jedes Element mit seiner toString()-Methode an, deshalb liefert der Record dort eine kurze Zeile wie „19.10.2026 07:00 zur Schule“.

FXML laden: Was FXMLLoader macht

Die Application-Klasse wird dadurch kürzer. Statt Nodes zu erzeugen, lädt sie die FXML-Datei und hängt das Stylesheet an die Scene:

@Override
public void start(Stage stage) throws IOException {
    FXMLLoader loader = new FXMLLoader(
            MitfahrlaborDesktopApp.class.getResource("hauptfenster.fxml"));
    Parent wurzel = loader.load();

    Scene scene = new Scene(wurzel, 800, 500);
    scene.getStylesheets().add(
            MitfahrlaborDesktopApp.class.getResource("mitfahrlabor.css").toExternalForm());

    stage.setTitle(TITEL);
    stage.setMinWidth(640);
    stage.setMinHeight(400);
    stage.setScene(scene);
    stage.show();
}

getResource("hauptfenster.fxml") sucht die Datei relativ zum Paket der Klasse, also unter de/dbh/mitfahrlabor/desktop/ im Ressourcenordner. Ein führender Schrägstrich ("/hauptfenster.fxml") würde im Wurzelverzeichnis suchen. Findet getResource nichts, liefert es null, und load() meldet „Location is not set“. Den Controller brauchst Du hier nicht; falls doch, liefert ihn loader.getController() nach dem Laden. Die Mindestgröße der Stage verhindert, dass die Bereiche beim Verkleinern ineinanderrutschen.

Was beim Aufruf von load() im Einzelnen passiert, zeigt das Sequenzdiagramm:

UML-Sequenzdiagramm: MitfahrlaborDesktopApp ruft load() am FXMLLoader auf; der Loader erzeugt die BorderPane und den HauptfensterController, erzeugt in einer Schleife je Element einen Kind-Node, setzt bei fx:id das gleichnamige @FXML-Feld, trägt bei onAction den Ereignis-Handler ein, ruft danach initialize() auf und gibt die Wurzel zurück; die App erzeugt damit die Scene (mit KI erstellt)

Wichtig ist die Reihenfolge: Der Controller entsteht früh, seine Felder werden beim Lesen der Elemente gesetzt, und erst ganz am Ende ruft der Loader initialize() auf. Darum dürfen im Konstruktor des Controllers noch keine Nodes benutzt werden.

Scene Builder: die Oberfläche zusammenklicken

FXML-Dateien musst Du nicht von Hand schreiben. Der Scene Builder ist ein freier grafischer Editor für FXML, den heute die Firma Gluon weiterentwickelt (Download unter gluonhq.com/products/scene-builder). Er hat drei Bereiche:

  • links die Library mit allen Containern und Controls und darunter die Hierarchy, also der Scene Graph als Baum,
  • in der Mitte die Vorschau, in die Du Elemente ziehst,
  • rechts den Inspector mit den Reitern Properties (z. B. text, Style-Klassen, Stylesheet), Layout (z. B. hgrow, rowIndex, Abstände) und Code (fx:id, onAction).

Unter „Controller“ links unten trägst Du die Controller-Klasse ein. „View → Show Sample Controller Skeleton“ erzeugt ein Gerüst mit allen @FXML-Feldern und Methoden, das Du in Deine Klasse kopieren kannst. Eclipse, IntelliJ IDEA und NetBeans können den Scene Builder als externen Editor für .fxml-Dateien öffnen.

Ein paar Hinweise für die Arbeit im Team:

  • Der Scene Builder speichert nur die FXML-Datei. Er schreibt alle Eigenschaften, die Du im Inspector berührst, als Attribute hinein, und er formatiert die Datei neu. Im Code-Review sind solche Änderungen schwer zu lesen; deshalb lohnt es sich, im Team zu vereinbaren, ob FXML von Hand oder mit dem Scene Builder gepflegt wird.
  • Gestaltung gehört ins Stylesheet: Farben und Schriften im Inspector (Bereich „Style“) landen als style="…" im FXML und überschreiben das Stylesheet. Besser ist eine Style-Klasse.
  • Die Vorschau zeigt auch eingebundene Stylesheets (Properties → Stylesheets), allerdings ohne Daten: Eine ListView bleibt leer, bis der Controller sie füllt.

Für diesen Artikel ist die FXML-Datei von Hand geschrieben, damit jede Zeile erklärt werden kann. Öffnest Du sie im Scene Builder, siehst Du dieselbe Hierarchie wie im Schema oben.

Gestalten mit JavaFX-CSS

Ohne Stylesheet zeigt JavaFX alle Nodes im Standard-Design Modena: korrekt angeordnet, aber ohne Abstände zum Fensterrand und ohne Hervorhebungen.

Hauptfenster der Desktop-App ohne eigenes Stylesheet: Kopfzeile, Liste mit zwei Fahrtangeboten, Details des ersten Angebots und Statuszeile stehen ohne Abstände und Farben direkt am Fensterrand (mit KI erstellt)

JavaFX gestaltet Nodes mit JavaFX-CSS. Die Schreibweise der Regeln (Selektor, geschweifte Klammern, Eigenschaft: Wert) kennst Du aus dem Web; wenn nicht, hilft der Artikel CSS-Grundlagen. Es gibt aber wichtige Unterschiede zum Browser-CSS:

CSS im Browser und JavaFX-CSS im Vergleich
CSS im Browser JavaFX-CSS
Eigenschaften color, background-color, padding mit Präfix: -fx-text-fill, -fx-background-color, -fx-padding
Selektor für einen Typ Element: p, button Style-Klasse: .label, .button, .list-cell (jede Control-Klasse hat eine)
eigene Klasse class="…" styleClass="…" (FXML) bzw. getStyleClass().add("…")
ID id="…" fx:id bzw. setId("…"), Selektor #…
Zustände :hover, :focus Pseudoklassen :hover, :focused, :selected, :disabled
Variablen --blau: …; var(--blau) eigene Eigenschaft -mitfahrlabor-blau: …; und direkte Verwendung als Wert
Layout display, flex, grid nicht über CSS, sondern über die Layout-Container
unbekannte Eigenschaft wird ignoriert wird ohne Meldung ignoriert

Der wichtigste Stolperstein steht in der letzten Zeile: Wer aus Gewohnheit background-color statt -fx-background-color schreibt, bekommt keine Fehlermeldung, die Regel wirkt einfach nicht.

Ein Auszug aus mitfahrlabor.css:

.root {
    -fx-font-size: 14px;
    -mitfahrlabor-blau: #1f5f99;   /* eigene Farbvariable */
}

.kopfzeile {
    -fx-background-color: -mitfahrlabor-blau;
    -fx-padding: 8 12 8 12;
}

.kopfzeile .label {
    -fx-text-fill: white;
}

.bereich {
    -fx-padding: 12;
}

.bereich-titel {
    -fx-font-size: 16px;
    -fx-font-weight: bold;
    -fx-text-fill: -mitfahrlabor-blau;
}

.button:focused {
    -fx-border-color: #f0b400;
    -fx-border-width: 2;
}

#angebotsliste .list-cell:selected {
    -fx-background-color: -mitfahrlabor-blau;
    -fx-text-fill: white;
}

Was die Regeln tun:

  • .root gilt für den Wurzelknoten jeder Scene. Werte wie die Schriftgröße erben alle Nodes darunter. Die selbst erfundene Eigenschaft -mitfahrlabor-blau wirkt wie eine Variable: Sie wird vererbt und kann in allen Regeln als Wert stehen.
  • .kopfzeile .label ist ein Nachfahren-Selektor: alle Labels innerhalb der Kopfzeile. So bleibt die weiße Schrift auf die Kopfzeile beschränkt.
  • -fx-padding: 8 12 8 12 nennt die Innenabstände in der Reihenfolge oben, rechts, unten, links, wie im Browser.
  • .button:focused hebt den Button mit dem Tastaturfokus deutlich hervor. Das ist Teil des zweiten Akzeptanzkriteriums.
  • #angebotsliste .list-cell:selected färbt die gewählte Zeile genau dieser Liste. Die Zeilen einer ListView sind eigene Nodes mit der Style-Klasse list-cell.

Stylesheets hängst Du an die Scene (scene.getStylesheets().add(…), gilt für alle Nodes) oder an einen einzelnen Parent (getStylesheets() eines Containers bzw. stylesheets="@mitfahrlabor.css" in FXML, wobei @ einen Pfad relativ zur FXML-Datei bezeichnet). Daneben gibt es das Attribut style="-fx-…" direkt am Node. Es hat den höchsten Vorrang und ist praktisch zum Ausprobieren, im fertigen Code aber schwer zu pflegen. Die Rangfolge ist: style-Attribut vor Stylesheet der Scene bzw. des Parents vor Modena; innerhalb eines Stylesheets entscheidet wie im Web die Spezifität (ID vor Klasse vor Typ).

Mit Stylesheet sieht das Hauptfenster so aus:

Hauptfenster der Desktop-App mit dem Stylesheet mitfahrlabor.css: blaue Kopfzeile mit weißer Schrift und Button Aktualisieren, links die Liste Meine Fahrtangebote mit blau markierter Auswahl, rechts die Details des Angebots vom 19.10.2026 um 07:00 Uhr zur Schule, unten eine hellblaue Statuszeile (mit KI erstellt)

Bedienung mit der Tastatur

Das zweite Akzeptanzkriterium verlangt, dass die Bedienelemente mit der Tastatur erreichbar sind. Vieles bringt JavaFX schon mit: Mit Tab und Umschalt+Tab springt der Fokus zwischen Button und Liste, in der Liste wählen die Pfeiltasten ein Angebot, und Leertaste bzw. Enter lösen einen fokussierten Button aus. Zwei Dinge ergänzt der Code-Stand:

  • Tastenkürzel (Mnemonics): Der Unterstrich in text="_Aktualisieren" zusammen mit mnemonicParsing="true" macht Alt+A zum Kürzel für den Button. Bei einem Label verweist labelFor auf das Feld, in das das Kürzel springen soll.
  • Sichtbarer Fokus: Die Regel .button:focused zeigt deutlich, wo der Fokus gerade steht.

Prüfe beides am Ende selbst: Maus weglegen, App starten und ein Angebot nur mit der Tastatur auswählen.

Typische Fehler mit FXML

Fehler in FXML-Dateien fallen erst zur Laufzeit auf, beim Aufruf von load(). Die Meldungen sind lang, die wichtige Information steht meist in der letzten Zeile „Caused by“ und in der Zeilennummer der FXML-Datei. Die folgenden Fälle sind am Code-Stand nachgestellt (Meldungen gekürzt):

Typische Fehler beim Laden von FXML
Symptom (Meldung) Ursache Abhilfe
LoadException: …hauptfenster.fxml:16 mit Caused by: ClassNotFoundException: de.dbh.mitfahrlabor.desktop.HauptfenstrController Tippfehler im Klassennamen von fx:controller vollen, richtigen Klassennamen eintragen
LoadException: Error resolving onAction='#aktualisiern', either the event handler is not in the Namespace or there is an error in the script. Methodenname in onAction passt nicht zum Controller Namen angleichen; Methode mit @FXML annotieren
LoadException: DatePicker is not a valid type. <?import …?> fehlt für eine verwendete Klasse Import ergänzen
LoadException mit Caused by: NullPointerException: Cannot invoke "…Label.setText(String)" because "this.detailDatum" is null (Aufruf in initialize()) fx:id und Feldname weichen ab (oder @FXML fehlt am privaten Feld); das Feld bleibt null fx:id und Feldname angleichen
InaccessibleObjectException: Unable to make field private … accessible: module de.dbh.mitfahrlabor.desktop does not "opens de.dbh.mitfahrlabor.desktop" to module javafx.fxml opens … to javafx.fxml fehlt in module-info.java opens-Zeile ergänzen
IllegalStateException: Location is not set. getResource(…) hat die FXML-Datei nicht gefunden (falscher Name, falscher Ordner) Datei unter src/main/resources/<paket>/ ablegen, Namen prüfen
keine Meldung, Gestaltung fehlt CSS-Eigenschaft ohne Präfix -fx- Präfix ergänzen

Ein Fall verdient besondere Aufmerksamkeit: Das fehlende opens fällt in den Tests nicht auf. Die Tests laufen auf dem Klassenpfad (wegen Monocle, siehe vorheriger Artikel), und dort gibt es keine Modulgrenzen. Erst gradlew run startet die App auf dem Modulpfad, und erst dort bricht sie ab. Das Team hat daraus eine Regel für die Spalte „Test“ des Boards gemacht: Wer prüft, startet die App einmal über gradlew run, statt sich nur auf die grüne Pipeline zu verlassen. Die Regeländerung steht mit Datum in board.md.

Testen und starten

Der Smoke-Test aus dem letzten Artikel prüft weiterhin, ob das Fenster startet; er sucht jetzt nach #angebotsliste und #details und prüft, dass ein Stylesheet angehängt ist. Neu ist ein Test, der die FXML-Datei ohne Fenster lädt und das Zusammenspiel mit dem Controller prüft:

@Test
@SuppressWarnings("unchecked")
void auswahlZeigtDetails() throws Exception {
    Parent wurzel = ladeHauptfenster();
    ListView<Angebot> liste = (ListView<Angebot>) wurzel.lookup("#angebotsliste");
    aufFxThread(() -> {
        liste.getSelectionModel().select(1);
        return null;
    });
    assertEquals("14:20 Uhr", ((Label) wurzel.lookup("#detailAbfahrt")).getText());
    assertEquals("nach Hause", ((Label) wurzel.lookup("#detailRichtung")).getText());
    assertEquals("3", ((Label) wurzel.lookup("#detailPlaetze")).getText());
    assertEquals("1,80 € je Person", ((Label) wurzel.lookup("#detailKosten")).getText());
}

ladeHauptfenster() ruft new FXMLLoader(…).load() auf dem JavaFX Application Thread auf, aufFxThread(…) führt die Auswahl dort aus (Muster mit FutureTask und Platform.runLater aus dem Smoke-Test). Ein dritter Test löst den Button mit fire() aus und prüft, dass onAction die Auswahl zurücksetzt. Damit sind fx:id, @FXML, initialize() und onAction abgesichert. Komfortablere GUI-Tests mit Mausklicks und Tastatureingaben folgen mit TestFX in einem späteren Artikel der Reihe.

.\gradlew.bat test     # Smoke-Test und Controller-Test (Maven: .\mvnw.cmd test)
.\gradlew.bat run      # Hauptfenster öffnen (Maven: .\mvnw.cmd javafx:run)

Den vollständigen Code-Stand mit FXML-Datei, Stylesheet, Controller und Tests findest Du unter src/03-fxml-layouts-css/mitfahrlabor-desktop.

Nächster Schritt: JFX-02 nach „Fertig“, JFX-03 ziehen

Beide Akzeptanzkriterien sind erfüllt: links die Liste, rechts die Details, alles in FXML und CSS, mit der Tastatur bedienbar. Die zweite Person im Test hat diesmal nicht nur die Pipeline angesehen, sondern die App gestartet, und JFX-02 wandert nach „Fertig“. Als Nächstes ziehst Du JFX-03 „Angebotsliste mit ViewModel und Beispieldaten“. Im Artikel Properties, Bindings und MVVM wandern die Daten aus dem Controller in ein ViewModel, das sich ohne Fenster testen lässt, und der Listener aus initialize() wird durch Bindings ersetzt.

Zum Üben gibt es die Übungsaufgaben zu FXML, Layouts und CSS: Dort ordnest Du Layout-Container zu, baust eine Maske für die Mitfahrgesuche in FXML mit Controller und Stylesheet und findest Fehler in einer FXML-Datei.

Leitfragen

  1. Welche Aufgaben übernehmen FXML-Datei, Stylesheet und Controller, und was gewinnt man durch die Trennung?

Die FXML-Datei beschreibt den Aufbau (welche Nodes in welchem Container), das Stylesheet das Aussehen (Farben, Schriften, Abstände), der Controller das Verhalten (Daten füllen, auf Ereignisse reagieren). Jede Datei bleibt dadurch übersichtlich, der Aufbau lässt sich mit dem Scene Builder bearbeiten, und die Gestaltung lässt sich ändern, ohne Java anzufassen. siehe Abschnitt „FXML trennt Aufbau und Verhalten“

  1. Welcher Layout-Container passt für ein Formular mit Beschriftung und Eingabefeld nebeneinander, welcher für eine Button-Leiste, welcher für das Grundgerüst eines Fensters?

Formular: GridPane (Beschriftungen in Spalte 0, Felder in Spalte 1, bündig untereinander). Button-Leiste: HBox (Kinder nebeneinander, ggf. mit wachsender Region als Abstandhalter). Grundgerüst: BorderPane mit top, left, center, right, bottom; center bekommt den restlichen Platz. siehe Abschnitt „Layout-Container“

  1. Wie verbinden sich eine FXML-Datei und ein Controller? Nenne die drei Stellen.

fx:controller am Wurzelelement nennt die Klasse mit vollem Namen. Ein Element mit fx:id="x" wird in das Feld x mit der Annotation @FXML geschrieben. onAction="#methode" ruft die gleichnamige @FXML-Methode des Controllers bei einem Klick auf. siehe Abschnitt „Der Controller“

  1. Warum gehört Code, der Nodes benutzt, in initialize() und nicht in den Konstruktor des Controllers?

Der FXMLLoader erzeugt den Controller früh und setzt die @FXML-Felder erst beim Lesen der Elemente. Im Konstruktor sind sie deshalb noch null. initialize() ruft der Loader erst auf, wenn alle Felder gesetzt sind. siehe Abschnitt „FXML laden“

  1. Worin unterscheidet sich JavaFX-CSS vom CSS im Browser? Nenne drei Unterschiede.

Eigenschaften tragen das Präfix -fx- (-fx-background-color, -fx-text-fill); Typen werden über Style-Klassen wie .label oder .button angesprochen; eigene Klassen setzt styleClass; Layout geschieht nicht über CSS, sondern über Container; unbekannte Eigenschaften werden ohne Meldung ignoriert. siehe Abschnitt „Gestalten mit JavaFX-CSS“

  1. Was ändert der Scene Builder, und worauf sollte ein Team bei seiner Nutzung achten?

Er bearbeitet nur die FXML-Datei, die er beim Speichern neu formatiert, und schreibt alle berührten Eigenschaften als Attribute hinein. Das Team sollte vereinbaren, ob FXML von Hand oder mit dem Scene Builder gepflegt wird, und Gestaltung als Style-Klasse ins Stylesheet statt als style-Attribut ins FXML legen. siehe Abschnitt „Scene Builder“

  1. Warum laufen die Tests grün, obwohl opens … to javafx.fxml fehlt, und wie fängt das Team den Fehler ab?

Die Tests laufen auf dem Klassenpfad, wo es keine Modulgrenzen gibt; der Zugriff des FXMLLoaders auf private Felder klappt dort immer. Erst der Start über gradlew run auf dem Modulpfad scheitert mit einer InaccessibleObjectException. Das Team hat die Regel der Spalte „Test“ ergänzt: Die prüfende Person startet die App einmal über gradlew run. siehe Abschnitt „Typische Fehler mit FXML“


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “FXML, Layouts und CSS: Das Hauptfenster der Desktop-App gestalten” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/javafx-fxml-layouts-css veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/javafx/javafx-einstieg. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: