Wie lässt sich JavaFX in Swing-Applikationen einbetten?

v.1.8Das gemeinsame Verwenden von JavaFX und Swing-Komponenten in einer Anwendung sollte nur in Ausnahmefällen in Erwägung gezogen werden, kann jedoch z.B. im Rahmen der Wartung und Erweiterung bestehender Anwendungen, gelegentlich sinnvoll oder sogar notwendig sein.

Die beiden UI-Frameworks verwenden jeweils eigene Threads für die Ausführung ihrer Benutzeroberflächen. Bei Swing ist dies der Event Dispatch Thread (EDT), bei JavaFX wird er als Application Thread bezeichnet. Der Zugriff auf die jeweiligen Komponenten darf nur aus den zugehörigen Threads heraus erfolgen. Hieraus erwächst die Notwendigkeit, beide strikt getrennt zu halten. Wie können sie dann wechselseitig aufeinander zugreifen?

Für die Einbettung von JavaFX in Swing wird die Komponente JFXPanel verwendet, die mittels setScene() die gewünschte FX-Scene übergeben bekommt. Das Panel ist von JComponent abgeleitet und stellt somit selbst eine Swing-Komponente dar, die gleichwohl zum Package javafx.embed.swing gehört. Sie muss allerdings so eingebettet werden, dass die Ausführung des UI auf dem Application Thread erfolgt. Dies geschieht durch die Methode Platform#runLater(Runnable r). Das Runnable r wird hierbei, wie bei Threads üblich, nach Übergabe an die Methode zu einem nicht genau planbaren Zeitpunkt ausgeführt.

import javax.swing.JFrame;

import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.geometry.HPos;
import javafx.geometry.VPos;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.web.WebView;


public class FXInSwing extends JFrame {

    private static final int WIDTH = 800;
    private static final int HEIGHT = 500;

    public FXInSwing() {
        initGUI();
    }

    public void initGUI() {
        final JFXPanel panel = new JFXPanel();
        this.add(panel);
        this.setTitle("JavaFX in Swing");
        this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        this.setSize(WIDTH, HEIGHT);
        this.setLocationRelativeTo(null);
        // Lambda Java 8
        Platform.runLater(() -> initJFXPanel(panel));
        this.setVisible(true);
    }

    private void initJFXPanel(JFXPanel panel) {
        Scene scene = new Scene(new Browser(), WIDTH, HEIGHT);
        panel.setScene(scene);
    }

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

class Browser extends Pane {

    final WebView view = new WebView();

    public Browser() {
        view.getEngine().load("http://yourwebs.de");
        getChildren().add(view);
    }

    @Override
    protected void layoutChildren() {
        double w = getWidth();
        double h = getHeight();
        layoutInArea(view, 0, 0, w, h, 0, HPos.CENTER, VPos.CENTER);
    }
}

Das Beispiel zeigt, wie eine Webseite mittels JavaFX innerhalb einer Swing-Anwendung dargestellt werden kann. Der Autor bevorzugte diese Lösung, als er bei der Entwicklung einer Hilfeseite für eine ältere Applikation die Darstellung einer zeitgemäßen, lokalen HTML-Datei benötigte und bekanntermaßen vor der Tatsache stand, dass in der Swing-Komponente JEditorPane lediglich HTML3 und nur rudimentäre Stylesheets unterstützt werden.

Die Klasse FXInSwing ist von JFrame abgeleitet und erzeugt damit eine Swing-Oberfläche als Hauptfenster der Anwendung. Im Konstruktor wir die Methode initGUI() aufgerufen, in der die Konfiguration des Fensters vorgenommen wird. Bevor dieses sichtbar gesetzt wird, wird ein Lambda-Ausdruck aufgerufen, der die Methode initJFXPanel() ausführt. Dort wird die FX-Scene erzeugt und auf das übergebene JFXPanel gesetzt. Wie oben erläutert greift Platform#runLater() auf den FX-Application-Thread zu, in dem die Scene ausgeführt werden kann.
Man erkennt hier, dass die Deklaration und Initialisierung des JFXPanel-Objektes problemlos auch auf dem EDT unter Swing erfolgen kann. Lediglich die Ausführung des UI muss im Application Thread erfolgen.

Der Lambda-Ausdruck ist erst ab Java 8 möglich, bei Verwendung einer früheren Version muss der Einzeiler durch die folgende Code-Stuktur ersetzt werden.

SwingUtilities.invokeLater(new Runnable() {
   @Override
   public void run() {
       initJFXPanel(panel);
   }
});

Wie erwähnt wird innerhalb von initJFXPanel() die FX-Scene an das Panel übergeben. Sie wird hier mit einem Objekt vom Typ Browser erzeugt, das wiederum von Pane, einer der Basisklassen für Layoutflächen in JavaFX, abgeleitet ist.
In der Klasse wird eine WebView erzeugt, die dem Pane hinzugefügt wird. Über die Methode getEngine() wird die WebEngine der View angesprochen, die dann durch load() den String der Seiten-URL als Parameter übergeben bekommt.

Quellen

http://docs.oracle.com/javase/8/javafx/embedded-browser-tutorial/overview.htm#JFXWV135