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:
1// Initialize FX runtime when the JFXPanel instance is constructed
2private synchronized static void initFx() {
3 // Note that calling PlatformImpl.startup more than once is OK
4 PlatformImpl.startup(() -> {
5 // No need to do anything here
6 });
7}
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:
Mit dem Laden des Videos akzeptieren Sie die Datenschutzerklärung von YouTube.
Mehr erfahren
YouTube immer entsperren
Und hier ist der Code für das JFXPanel
:
1public class SwingFXPanel extends JFXPanel {
2
3 private Button testButton;
4 private TextField testTextField;
5 private Label testLabel;
6
7 public SwingFXPanel() {
8 init();
9 }
10
11 private void init() {
12 testButton = new Button("I am a JavaFX Button");
13 testTextField = new TextField();
14 testLabel = new Label("empty");
15 VBox pane = new VBox(testTextField, testButton, testLabel);
16 pane.setAlignment(Pos.CENTER);
17 setScene(new Scene(pane));
18 }
19
20 public Button getTestButton() {
21 return testButton;
22 }
23
24 public TextField getTestTextField() {
25 return testTextField;
26 }
27
28 public Label getTestLabel() {
29 return testLabel;
30 }
31
32}
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…
1public class SwingPanel extends JPanel{
2
3 private JButton testButton;
4 private JTextField testTextField;
5 private JLabel testLabel;
6
7 public SwingPanel() {
8 init();
9 }
10
11 private void init(){
12 setLayout(new BorderLayout());
13 JPanel panel = new JPanel();
14 panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
15 testButton = new JButton("I am a Swing Button");
16 testTextField = new JTextField();
17 testLabel = new JLabel("empty");
18 testButton.setAlignmentX(Component.CENTER_ALIGNMENT);
19 testTextField.setAlignmentX(Component.CENTER_ALIGNMENT);
20 testLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
21 Box.Filler filler1 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 1000), new Dimension(0, 32767));
22 Box.Filler filler2 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 1000), new Dimension(0, 32767));
23 panel.add(filler1);
24 panel.add(testTextField);
25 panel.add(testButton);
26 panel.add(testLabel);
27 panel.add(filler2);
28 add(panel, BorderLayout.CENTER);
29 }
30
31 public JButton getTestButton() {
32 return testButton;
33 }
34
35 public JLabel getTestLabel() {
36 return testLabel;
37 }
38
39 public JTextField getTestTextField() {
40 return testTextField;
41 }
42
43}
… und für das Demo-Fenster:
1public class InteropFrame extends JFrame {
2
3 private JSplitPane centralSplitPane;
4 private SwingPanel swingPanel;
5 private SwingFXPanel swingFXPanel;
6
7 public InteropFrame() {
8 init();
9 }
10
11 private void init() {
12 setTitle("Swing - JavaFX Interoperability");
13 setSize(800, 500);
14 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15 setLayout(new BorderLayout());
16 centralSplitPane = new JSplitPane();
17 swingPanel = new SwingPanel();
18 swingFXPanel = new SwingFXPanel();
19 swingPanel.getTestButton().addActionListener((ActionEvent e) -> {
20 Platform.runLater(() -> {
21 swingFXPanel.getTestLabel().setText(swingPanel.getTestTextField().getText());
22 });
23 });
24 swingFXPanel.getTestButton().setOnAction(a -> {
25 swingPanel.getTestLabel().setText(swingFXPanel.getTestTextField().getText());
26 });
27 centralSplitPane.setLeftComponent(swingPanel);
28 centralSplitPane.setRightComponent(swingFXPanel);
29 add(centralSplitPane, BorderLayout.CENTER);
30 }
31
32}
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…
1<VBox alignment="CENTER" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"> 2 <children> 3 <TextField fx:id="testTextField" /> 4 <Button fx:id="testButton" mnemonicParsing="false" text="I am a JavaFX Button" /> 5 <Label fx:id="testLabel" text="empty" /> 6 </children> 7</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:
1public class SwingFXMLPanel extends JFXPanel {
2
3 @FXML
4 private Button testButton;
5 @FXML
6 private TextField testTextField;
7 @FXML
8 private Label testLabel;
9 private VBox pane;
10
11 public SwingFXMLPanel() {
12 init();
13 }
14
15 private void init() {
16 FXMLLoader loader = new FXMLLoader(getClass().getResource("demo.fxml"));
17 loader.setController(this);
18 try {
19 loader.load();
20 } catch (IOException ex) {
21 Logger.getLogger(SwingFXMLPanel.class.getName()).log(Level.SEVERE, null, ex);
22 }
23 pane = loader.getRoot();
24 Platform.runLater(this::createScene);
25 }
26
27 public void createScene() {
28 Scene scene = new Scene(pane);
29 setScene(scene);
30 }
31
32 public Button getTestButton() {
33 return testButton;
34 }
35
36 public TextField getTestTextField() {
37 return testTextField;
38 }
39
40 public Label getTestLabel() {
41 return testLabel;
42 }
43}
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.
Weitere Beiträge
von Jens Deters
Dein Job bei codecentric?
Jobs
Agile Developer und Consultant (w/d/m)
Alle Standorte
Gemeinsam bessere Projekte umsetzen.
Wir helfen deinem Unternehmen.
Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.
Hilf uns, noch besser zu werden.
Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.
Blog-Autor*in
Jens Deters
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.
Du hast noch Fragen zu diesem Thema? Dann sprich mich einfach an.