Was ist und wozu dient FXML?

FXML ist eine XML-basierte Markup-Sprache, die größtenteils verwendet wird, um den Objekt-Baum von mit JavaFX erstellten Benutzeroberflächen zu spezifizieren.

v.8.0JavaFX ist ein Framework, das in einigen Bereichen durchaus geeignet sein könnte, die Nachfolge der erfolgreichen Swing-Bibliothek anzutreten. Seine Anwendung folgt dem Model-View-Controller-Entwurfsmuster (MVC), sodass graphische Oberflächen von den Rechenroutinen und vom Datenpool getrennt bleiben.

Diese Trennung erlaubt es, die Entwicklung einer graphischen Nutzeroberfläche weitgehend vom Rest des Programmentwurfs zu entkoppeln. In einer *.fxml-Datei kann hierbei der vollständige Baum aller graphischen Objekte festgelegt werden. Dies kann sowohl in einem XML- oder einfachen Texteditor erfolgen, als auch im JavaFX Scene Builder, einem hierfür speziell entwickelten WYSIWYG-Editor1.
Die Gestaltung einer Nutzeroberfläche mit Hilfe von FXML bietet hierbei die folgenden Vorteile:

Das hier behandelte Beispiel zeigt eine einfache Anwendung, die, nach Klick auf einen Button, einen Text auf einem Label zeigt.

Die konventionelle prozedurale Variante

Wird die Oberfläche konventionell programmiert, so könnte der Quelltext etwa folgendermaßen aussehen:

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class FXMLBsp extends Application {
    
    @Override
    public void start(Stage primaryStage) throws Exception{
        GridPane root = new GridPane();
        Button button = new Button("Klick");
        button.setPrefWidth(100.0);
        root.add(button,0,0 );
        Label label = new Label();
        label.setPrefWidth(100.0);
        label.setAlignment(Pos.CENTER);
        root.add(label,0,1 );
        
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                label.setText(new FXMLBspModel().getHello());
            }
        }); 
        primaryStage.setTitle("FXML-Beispiel");
        primaryStage.setScene(new Scene(root, 100, 57));
        primaryStage.show();
    }

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

Die Oberfläche wird hier vollständig in start() implementiert. Dieses Verfahren ist natürlich der Einfachheit der Oberfläche und dem Demonstrationsziel geschuldet.
Button und Label werden untereinander auf ein GridPane gesetzt, das schließlich einem scene-Objekt übergeben wird. Zur weiteren Demonstration werden den Komponenten zusätzlich einige Eigenschaften hinzugefügt und die Schaltfläche erhält in einer anonymen Klasse seine Funktionalität. Durch einen Klick wird aus einer Model-Klasse über einen Getter eine Instanzvariable ausgelesen und deren Wert auf das Label gesetzt.
Stellt man sich hier eine deutlich komplexeres GUI vor, so wird die Unübersichtlichkeit deutlich: Der scene graph, die Hierarchie der Komponenten, ist nur schwer und durch intensive Einarbeitung in den Text nachzuvollziehen.

Die FXML-Variante

Um den Klassenquelltext für die Verwendung einer *.fxml-Datei vorzubereiten, ist nur ein Eingriff nötig: Die gesamte Routine zur Erzeugung der Oberfläche wird einschließlich der Importanweisungen gelöscht und durch eine Anweisung ersetzt, mit der die erwähnte Datei geladen wird.
Um eine größere Flexibilität zu haben und das hier verwendete GridPane beliebig auswechseln zu können, wird hier eine Variable vom Typ Parent verwendet. Die Klasse stellt den Supertyp aller Knoten dar, die Kinder in der graphischen Hierarchie haben können.
Das Laden selbst geschieht durch die statische Methode load() eines FXMLLoaders. Hier muss der relative Pfad zur *.fxml-Datei angegeben werden.

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class FXMLBsp extends Application {
    
    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("FXMLBsp.fxml")); 
        primaryStage.setTitle("FXML-Beispiel");
        primaryStage.setScene(new Scene(root, 100, 57));
        primaryStage.show();
    }

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

Die Struktur einer *.fxml-Datei entspricht mit einigen Ausnahmen der gängigen Syntax einer xml-Datei. Die genaue, ausführliche Dokumentation ist unter http://docs.oracle.com/javafx/2/api/javafx/fxml/doc-files/introduction_to_fxml.html zu finden.

Wie im Java-Quelltext auch, so können auch in *.fxml-Dateien Klassennamen entweder voll qualifiziert, mit Package-Angabe, oder aber per Import-Anweisung angegeben werden.
Im vorliegenden Beispiel werden die aus dem Klassenquelltext gelöschten Import-Anweisungen der verwendeten Komponenten mit anderer Syntax zu Beginn der Datei eingefügt. Sie müssen innerhalb von <?import ... ?>-Elementen formuliert werden.
Wie in xml-Dateien kann ein Element entweder aus Start- und End-Tag bestehen oder, bei fehlendem End-Tag, einen schließenden Schrägstrich '/' besitzen.
Einen Unterschied gibt es bei der Groß-/Kleinschreibung: Mit großem Anfangsbuchstaben geschriebene Elemente werden als Objektdeklarationen behandelt, die der FXMLLoader zum Anlass nimmt, entsprechende Objektbildungen vorzunehmen. Kleine Anfangsbuchstaben definieren Eigenschaften, die wahlweise in Attribut-Form oder als Kindelemente geschrieben werden können.

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

<?import java.lang.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.GridPane?>

<GridPane xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2"
    fx:controller="frameworks.javafx.fxml.FXMLBspController">
    <children>
        <Button fx:id="button" onAction="#manageButton" prefWidth="100.0"
            text="Klick!" GridPane.columnIndex="0" GridPane.rowIndex="0" />
        <Label fx:id="label" alignment="CENTER" prefWidth="100.0" text=""
            GridPane.columnIndex="0" GridPane.rowIndex="1" />
    </children>
</GridPane>

Die Klassenangabe eines importierten, von Node abgeleiteten Typs, hier GridPane, bildet das Wurzelelement. Als Argument werden diesem die Namensraumangaben für JavaFX und FXML, sowie ein Controller-Name (incl. Packageangabe) als Wert des Attributs fx:controller übergeben. Innerhalb des Elementes children werden dann die Elemente spezifiziert, die durch das GridPane angeordnet werden sollen.
Sie erhalten Attribute, deren Bezeichner von den Setter-Methoden der Klasse (ohne 'set), abgeleitet sind und zusätzlich ein Attribut fx:id, das eine ID definiert, mit der das erzeugte Objekt im Quelltext angesprochen wird.

Der Controller

Über das Attribut onAction wird im Button-Element ein Methodenname mit vorangestelltem Doppelkreuz (#) angegeben, über den im o.a. Controller die Ereignisbehandlung erfolgt.
Dieser stellt eine gängige Java-Klasse mit beliebiger Bezeichnung dar, die oftmals das Interface Initializable implementiert, dies jedoch nicht zwingend muss. Das Interface stellt die Methode initialize() bereit. Sie wird beim Aufruf des Controllers einmal aufgerufen. Im vorliegende Fall wird ein Objekt einer einfache Model-Klasse erzeugt, die den Text bereitstellt, der auf dem Label dargestellt wird.

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

import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;

public class FXMLBspController implements Initializable {

    @FXML
    private Label label;

    private FXMLBspModel model;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        model = new FXMLBspModel();
    }

    public void manageButton() {
        label.setText(model.getHello());
    }
}

Zu Erwähnen ist noch die Deklaration des Labels. Sie ist mit der annotation @FXML versehen. Dies bewirkt, dass das private Objekt wie üblich zwar nicht von anderen Klassen, jedoch von der Markupsprache aus angesprochen werden kann.

Das Model

public class FXMLBspModel {
    
    private String hello = "Hello World!";
    
    public String getHello() {
        return hello;
    }
}

1) Im Gegensatz zu den meisten bislang üblichen graphischen Editoren für Swing-Oberflächen, kann und darf eine mit dem Scene Editor erstellte *.fxml-Datei problemlos auch auf andere Art und Weise nachbearbeitet werden.