Swing und JavaFX – Migration und Rundumerneuerung?
Alte Swing-Anwendungen, die seit Jahren in Produktion sind und weiterhin gepflegt werden müssen, sind keine Seltenheit. Dabei wird die Wartung der historisch gewachsenen Software immer ineffizienter und neu geforderte Features sind umständlich zu implementieren – wenn es überhaupt möglich ist, sämtliche Kundenanforderungen mit der in die Jahre gekommenen Technologie umzusetzen.
Die Gründe für die Modernisierung einer Swing-Anwendung sind vielfältig. Wenn die Entscheidung gefallen ist, die Anwendung mit JavaFX zu erneuern, stellt sich natürlich die Frage, ob es dazu technische Hilfsmittel seitens JavaFX gibt. Diese Hilfsmittel gibt es. Zum einen können JavaFX-Komponenten in Swing, zum anderen können Swing-Komponenten in JavaFX eingebettet werden. Wir stellen zwei Werkzeuge für unterschiedliche Migrationsansätze vor.
Möglichkeit 1 – JavaFX in Swing
Mit der Komponente javafx.embed.swing.JFXPanel ist es möglich, JavaFX-Inhalte in eine bestehende Swing-Anwendung zu integrieren. An dieser Stelle gibt es zwei Anwendungsfälle:
- Schrittweise Erneuerung der Anwendung von innen heraus
Ein Beispiel dafür wäre: Zunächst werden alle Dialoge erneuert, anschließend die restlichen Bereiche und abschließend wird der alte Swing-Anwendungsrahmen auf JavaFX umgestellt. An dieser Stelle sei bereits erwähnt: Dies ist nicht trivial, erhält jedoch die Release-Fähigkeit der Anwendung. - Neue Features mit JavaFX
Sollten neue Features zur Anwendung hinzukommen, lohnt es sich nicht mehr, diese mit der alten Technologie umzusetzen, da die Migration sowieso ansteht. Um an der Stelle die Investitionen zu sichern, lohnt eine direkte JavaFX-Implementierung, um zur eigentlichen Migration nicht unnötig Umstellungsaufwand zu erzeugen.
Kommen wir nun zu einem konkreten Beispiel. Im Folgenden ist eine Swing-Anwendung programmiert, welche einen javax.swing.JButton und einen javafx.scene.control.Button untereinander anzeigt. Es wird also JavaFX-Content in Swing eingebettet.
public class SwingFrame extends JFrame {
public SwingFrame() {
super("Swing Frame");
setLayout(new GridLayout(0, 1));
JButton swingButton = new JButton("Swing Button");
// Create JavaFX Container
JFXPanel jfxPanel = new JFXPanel();
// Switch to JavaFX Thread to create JavaFX content
Platform.runLater(() -> {
Button btJavaFX = new Button("JavaFX Button");
jfxPanel.setScene(new Scene(btJavaFX));
SwingUtilities.invokeLater(() -> {
// Switch to Swing Thread, because we need to layout all stuff, when the JavaFX content loaded
pack();
setVisible(true);
});
});
getContentPane().add(swingButton);
getContentPane().add(jfxPanel);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new SwingFrame());
}
}
Das Beispiel zeigt, welche Hürden zu meistern sind: Sowohl JavaFX als auch Swing verwenden einen eigenen UI-Thread. Änderungen an der jeweiligen UI müssen aus dem jeweiligen Thread getätigt werden, sonst können Layouting-Probleme entstehen bzw. Exceptions (siehe im Folgenden) auftreten.
java.lang.IllegalStateException: Not on FX application thread
Um dies zu umgehen, bieten beide Toolkits die Möglichkeit, von einem nicht-UI-Thread aus Operationen im UI-Thread auszuführen. Dabei gibt es zwei Wege:
Swing
SwingUtilities.invokeLater(() -> //Runnable with operations regarding Swing UI)
JavaFX
Platform.runLater(() -> //Runnable with operations regarding JavaFX UI)
Bei beiden Aufrufen wird ein Runnable übergeben, welches an das Ende der jeweiligen Event- Queue gehängt und anschließend im UI-Thread ausgeführt wird. Im Beispiel fällt auf, dass mehrmals zwischen den Threads hin und her gesprungen wird. Dadurch werden Layout-Probleme umgangen, denn #pack() wird erst nach dem Erstellen des JavaFX-Contents aufgerufen. Sollte #pack() nicht nach dem Erstellen des JavaFX-Contents aufgerufen werden sondern früher, wären die Bounds des JavaFX-Contents noch nicht berechnet und würden damit auch nicht in die Layout-Berechnung einbezogen werden. Das Resultat ist ein zu kleines Anzeigefenster.
Die nächste Herausforderung ist im Screenshot zu erkennen. Es fällt ein Bruch im Look & Feel auf – ein Problem, das wiederum nicht einfach zu lösen ist. Eine Möglichkeit ist, JavaFX-Komponenten mit CSS dem Swing-L&F anzunähern.
Möglichkeit 2 – Swing in JavaFX
Das javafx.embed.swing-SwingNode kommt zum Einsatz, wenn das Anwendungsgerüst bereits auf JavaFX umgestellt ist, jedoch bestimmte Alt-Komponenten weiterhin verwendet werden. Ein Beispiel dafür ist eine komplexe JTable die Excel nachempfindet und im ersten Migrationsschritt aus Zeitgründen noch nicht neu implementiert werden soll.
Kommen wir wieder zu einem konkreten Beispiel. Ähnlich wie beim letzten Beispiel, handelt es sich um eine JavaFX-Anwendung, die einen javax.swing.JButton und einen javafx.scene.control.Button darstellt. An dieser Stelle wird also Swing in JavaFX eingebettet.
public class JfxApplication extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Button label = new Button("JavaFX");
SwingNode swing = new SwingNode();
SwingUtilities.invokeLater(() -> {
JButton jLabel = new JButton("Swing");
swing.setContent(jLabel);
Platform.runLater(() -> {
VBox vBox = new VBox(label, swing);
Scene scene = new Scene(vBox);
primaryStage.setScene(scene);
primaryStage.show();
});
});
}
public static void main(String[] args) {
launch(args);
}
}
Die Herausforderungen sind dieselben wie gehabt – Es muss darauf geachtet werden, wie zwischen den Threads kommuniziert wird und auch die L&F-Probleme sind wieder offensichtlich.
Fazit
Mit SwingNode und JFXPanel können Swing und JavaFX verheiratet werden. Entgegen dem echten Leben sollte diese Vereinigung jedoch nur temporär sein. Das eigentliche Ziel ist es nicht, einen möglichst komplexen Mix aus Swing und JavaFX zu erzeugen. Ziel ist es, die Technologien nur punktuell zu verknüpfen und die Altanwendung zeitnah komplett mit JavaFX abzulösen.