Integration von JavaFX in Swing-Applikationen

Keine Kommentare

Eines vorweg: Bei Überlegungen, einen bestehenden Swing-Client in Richtung JavaFX zu migrieren, sollte dringend die Option geprüft werden, eine von Grund auf neue JavaFX-Applikation zu entwickeln und nicht Swing mit JavaFX (oder umgekehrt) zu vermischen!

Sollen dennoch JavaFX-Inhalte in eine Swing-Applikation eingebettet werden (ist mir in der Praxis durchaus schon über den Weg gelaufen), steht im Package javafx.embed.swing die Klasse JFXPanel bereit. Ein JXFPanel ist vom Typ javax.swing.JComponent abgeleitet und kann somit einfach direkt in Swing verwendet werden.

Swing – JavaFX – Interoperability

Swing und JavaFX sind jeweils Single Threaded UI Toolkits, wobei der (AWT) Event Dispatch Thread (EDT) für Swing automatisch mit der JVM gestartet wird, während der JavaFX Platform Thread explizit gestartet werden muss.

Praktischerweise macht dies der Konstruktor des JFXPanel durch initFX() gleich mit:

// Initialize FX runtime when the JFXPanel instance is constructed
private synchronized static void initFx() {
    // Note that calling PlatformImpl.startup more than once is OK
    PlatformImpl.startup(() -> {
        // No need to do anything here
    });
}

Aus dem EDT können JavaFX-Controls aber nur über den JavaFX Platform Thread angesprochen werden. Dazu wird ein Runnable über die Klasse javafx.application.Platform.runLater(Runnable run) eingereicht. Ein Versuch, aus einem anderen Thread heraus, z. B. den Text von einem javafx.scene.control.Label zu ändern, wird mit einer
java.lang.IllegalStateException: Not on FX application thread; currentThread = AWT-EventQueue-0
geahndet!

Hier sind zur Verdeutlichung zwei funktional identische Panels in Swing und JavaFX, jeweils mit einem Button, einem TextField und einem Label erstellt. Beide Panels sind in ein javax.swing.JFrame eingebettet und können jeweils den Inhalt des Textfeldes an ein Label der „Gegenseite“ übergeben:

Und hier ist der Code für das JFXPanel:

public class SwingFXPanel extends JFXPanel {
 
    private Button testButton;
    private TextField testTextField;
    private Label testLabel;
 
    public SwingFXPanel() {
        init();
    }
 
    private void init() {
        testButton = new Button("I am a JavaFX Button");
        testTextField = new TextField();
        testLabel = new Label("empty");
        VBox pane = new VBox(testTextField, testButton, testLabel);
        pane.setAlignment(Pos.CENTER);
        setScene(new Scene(pane));
    }
 
    public Button getTestButton() {
        return testButton;
    }
 
    public TextField getTestTextField() {
        return testTextField;
    }
 
    public Label getTestLabel() {
        return testLabel;
    }
 
}

Vor Java 8u40 war es nötig, die javafx.scene.Scene via Platform.runLater() zu erzeugen, denn die Scene hat im Konstruktor via Toolkit.getToolkit().checkFxUserThread() andernfalls für einen Abbruch gesorgt. Seit 8u40 wird dieses nicht mehr so streng gehandhabt und es ist erlaubt, eine Scene auch direkt aus dem EDT heraus zu erstellen.

Der Code für das SwingPanel…

public class SwingPanel extends JPanel{
 
    private JButton testButton;
    private JTextField testTextField;
    private JLabel testLabel;
 
    public SwingPanel() {
        init();
    }
 
    private void init(){
        setLayout(new BorderLayout());
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        testButton = new JButton("I am a Swing Button");
        testTextField = new JTextField();
        testLabel = new JLabel("empty");
        testButton.setAlignmentX(Component.CENTER_ALIGNMENT);
        testTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
        testLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        Box.Filler filler1 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 1000), new Dimension(0, 32767));
        Box.Filler filler2 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 1000), new Dimension(0, 32767));
        panel.add(filler1);
        panel.add(testTextField);
        panel.add(testButton);
        panel.add(testLabel);
        panel.add(filler2);
        add(panel, BorderLayout.CENTER);
    }
 
    public JButton getTestButton() {
        return testButton;
    }
 
    public JLabel getTestLabel() {
        return testLabel;
    }
 
    public JTextField getTestTextField() {
        return testTextField;
    }
 
}

… und für das Demo-Fenster:

public class InteropFrame extends JFrame {
 
    private JSplitPane centralSplitPane;
    private SwingPanel swingPanel;
    private SwingFXPanel swingFXPanel;
 
    public InteropFrame() {
        init();
    }
 
    private void init() {
        setTitle("Swing - JavaFX Interoperability");
        setSize(800, 500);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        centralSplitPane = new JSplitPane();
        swingPanel = new SwingPanel();
        swingFXPanel = new SwingFXPanel();
        swingPanel.getTestButton().addActionListener((ActionEvent e) -> {
            Platform.runLater(() -> {
                swingFXPanel.getTestLabel().setText(swingPanel.getTestTextField().getText());
            });
        });
        swingFXPanel.getTestButton().setOnAction(a -> {
            swingPanel.getTestLabel().setText(swingFXPanel.getTestTextField().getText());
        });
        centralSplitPane.setLeftComponent(swingPanel);
        centralSplitPane.setRightComponent(swingFXPanel);
        add(centralSplitPane, BorderLayout.CENTER);
    }
 
}

Man beachte den ActionListener, der am JButton registriert wird: Bei der Interaktion muss unbedingt darauf geachtet werden, dass JavaFX Controls via Platform.runLater() über den JavaFX Platform Thread angesprochen werden.

FXML

Alternativ kann natürlich auch im JFXPanel die UI als FXML geladen werden. Damit profitiert auch ein Swing-Client von den Vorteilen einer deklarativen Oberfläche und von Tools wie dem SceneBuilder.

Die FXML-Datei gestaltet sich recht übersichtlich…

<VBox alignment="CENTER" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <TextField fx:id="testTextField" />
        <Button fx:id="testButton" mnemonicParsing="false" text="I am a JavaFX Button" />
        <Label fx:id="testLabel" text="empty" />
    </children>
</VBox>

… und wird dann über den FXMLLoader geladen. Durch loader.setController(this) werden dann die mit @FXML annotierten JavaFX-Controls beim Laden durch den FXMLLoader injiziert:

public class SwingFXMLPanel extends JFXPanel {
 
    @FXML
    private Button testButton;
    @FXML
    private TextField testTextField;
    @FXML
    private Label testLabel;
    private VBox pane;
 
    public SwingFXMLPanel() {
        init();
    }
 
    private void init() {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("demo.fxml"));
        loader.setController(this);
        try {
            loader.load();
        } catch (IOException ex) {
            Logger.getLogger(SwingFXMLPanel.class.getName()).log(Level.SEVERE, null, ex);
        }
        pane = loader.getRoot();
        Platform.runLater(this::createScene);
    }
 
    public void createScene() {
        Scene scene = new Scene(pane);
        setScene(scene);
    }
 
    public Button getTestButton() {
        return testButton;
    }
 
    public TextField getTestTextField() {
        return testTextField;
    }
 
    public Label getTestLabel() {
        return testLabel;
    }
}

Fallstricke

In der Praxis wird man vermutlich das JFXPanel nicht nur ein einziges Mal in der Applikation benutzen und bestimmt diese auch zur Laufzeit dynamisch erzeugen und wieder verwerfen.
Verwendet man zum Beispiel ein JFXPanel in einem JDialog wird, wie bereits erwähnt, der JavaFX Platform Thread vom JXFPanel gestartet, falls dieser nicht bereits läuft.
Allerdings sorgt das JFXPanel auch dafür, dass der Platform Thread beendet wird wenn die letzte Instanz von JFXPanel „stirbt“ (Platform.exit()). D.h. in diesem Fall wenn der JDialog beendet wird. Beim zweiten Instanziieren des Dialogs folgt dann eine Exception:

Exception in thread "AWT-EventQueue-0" java.lang.IllegalStateException: Platform.exit has been called

Denn: Der JavaFX Platform Thread lässt sich nicht erneut starten, wenn dieser einmal beendet wurde. Das JFXPanel bleibt in diesem Fall aus Benutzersicht einfach leer.

Dieses implizite Schließen des JavaFX Platform Threads kann aber recht einfach verhindern werden:
Platform.setImplicitExit(false)
schaltet diesen Automatismus ab. Am besten ist, diesen Aufruf bereits ganz oben in main() zu setzen. Der JavaFX Platform Thread wird dann erst durch den expliziten Aufruf von Platform.exit() beendet.

Fazit

Wie eingangs erwähnt, ist es nicht angeraten, Swing und JavaFX zu stark zu vermischen. Eine vollständige Neuimplementierung in JavaFX ist der ausdrücklich zukunftsfähigere und qualitativ hochwertigere Weg!

Im Übrigen stellt es einen nicht unerheblichen Aufwand dar, den Stil so anzupassen, dass der Benutzer eine konsistente Oberfläche erlebt. Vor allem vor dem Hintergrund, dass Swing in der Regel versucht das jeweilige native Look-and-Feel anzunehmen, JavaFX-UIs dagegen standardmäßig auf jeder Plattform (in etwa) gleich aussehen.

Es kann aber durchaus Anwendungsfälle geben, bei denen bis auf Weiteres der Swing-Client beibehalten werden soll und man aber trotzdem von JavaFX-Komponenten wie z. B. der WebView oder JavaFX Media profitieren möchte.

Der obige Beispielcode ist HIER zu finden.

Jens Deters

Etwa 25 Jahre ist es her, dass Jens Deters mit dem Home-Computing begonnen hat. Damals zogen ihn Computer in ihren Bann, und die letzten 15 Jahren hatte er verschiedenste Rollen im IT- und Telekommunikationsumfeld inne (Software Entwickler, Trainer, Berater, Projekt- und Produktmanager). Heute arbeitet er bei codecentric als Senior IT Consultant. Er schreibt regelmäßig über seine Projekte (www.jensd.de, www.mqttfx.org) und trägt zur JavaFX- und IoT-Community bei. Jens ist Mitglied des NetBeans Dream Teams.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.