The other day I was taking a look at JavaFX and how it might fit one of my
OSGi related projects. As usual things are not as easy as they appear at
first sight, so it is time to add JavaFX to osgisnippets.
To use JavaFX with my other project and with osgisnippets, the following
goals exist:
- Ability to show Swing components
- Provide a guitool based on JavaFX like the one based on Swing
- Get it to work with OSGi
Goal 1 is covered in this article. The others will be covered in their own
articles soon. For the tests I created the class SnipFXSwing. A link to the sources
is at the end of the article. In the source archive,
it is in the guitool project in package example.guitool.snip.
JFXPanel - embed JavaFX in Swing
This is straight forward, the API doc explains how. (See the Resources
section at the end of the article.)
Note that JFXPanel is about having a Swing application and starting JavaFX
components in it. I need it the other way around.
Preparation
The class SnipFXSwing has a main method in which different start methods
are used depending on the command line argument:
public static void main(final String[] args) {
if (args.length<1) {
System.err.println(
"Usage:\n" +
"SnipFXSwing fx starts FX GUI\n" +
"SnipFXSwing awtfx starts fx after AWT init\n" +
"SnipFXSwing swfx starts fx within Swing");
return;
}
switch (args[0]) {
case "fx": startFX(args); break;
case "awtfx": startAWTFX(args); break;
case "swfx": startSwFX(args); break;
default:
System.err.println("Error: argument " + args[0] + " not supported");
}
}
Approach 1 - Just Try It
Lets just fire up a JavaFX UI and try to create a Swing JFrame. To do that,
SnipFXSwing is started with the argument fx. The relevant code is this:
public class SnipFXSwing extends Application {
...
private static void startFX(String[] args) {
Application.launch(SnipFXSwing.class, args);
}
...
Stage mainStage;
...
ToolBar mainToolBar;
Button exitButton;
Button showSwingButton;
@Override
public void start(Stage stage) throws Exception {
mainStage = stage;
mainStage.setTitle("Snippet JavaFX with Swing");
Scene mainScene = createMainScene();
mainStage.setScene(mainScene);
mainStage.setOnCloseRequest(new EventHandler<WindowEvent>() {
@Override
public void handle(WindowEvent event) {
if (mainStage!=null) {
doExit();
}
}
});
mainStage.show();
}
}
This is typical source code for small JavaFX applications. The method
createScene creates the JavaFX components. This separate method exists,
because this part will be reused later. The Stage is the main
window and a handler is registered to shut down the application when the window
is closed.
The method doShowSwing is called, when the button in the center of the
window is pressed:
protected void doShowSwing() {
if (!SwingUtilities.isEventDispatchThread()) {
// re-run in EDT:
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
doShowSwing();
}
});
return;
}
JFrame f = new JFrame("Swing Window");
f.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
f.getContentPane().add(new JLabel("This is a Swing JFrame"));
f.setSize(400, 200);
f.setLocationRelativeTo(null);
f.setVisible(true);
}
The first if statement checks, whether the method has been called from the
Event Dispatch Thread (EDT) and calls it from the EDT if not. The remaining
code is the usual way to create a Swing JFrame. When running on Mac OS X and
pressing the button "Show Swing Window", this error is logged:
java[10613:707] [JRSAppKitAWT markAppIsDaemon]: Process manager already initialized: can't fully enable headless mode.
Exception in thread "AWT-EventQueue-0" java.awt.HeadlessException
at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:207)
at java.awt.Window.<init>(Window.java:535)
at java.awt.Frame.<init>(Frame.java:420)
at javax.swing.JFrame.<init>(JFrame.java:224)
at example.guitool.snip.SnipFXSwing.doShowSwing(SnipFXSwing.java:231)
...
So that doesn't work. There are a few issues which require, that AWT is
initialized before JavaFX. (Link to bug reports in the Resources section.)
BTW: If the if block is commented out and the code is not run in the EDT,
the following error is logged:
java[10698:707] [JRSAppKitAWT markAppIsDaemon]: Process manager already initialized: can't fully enable headless mode.
Glass detected outstanding Java exception at -[GlassViewDelegate sendJavaMouseEvent:]:src/com/sun/mat/ui/GlassViewDelegate.m:541
Exception in thread "AWT-AppKit" java.awt.HeadlessException
at java.awt.GraphicsEnvironment.checkHeadless(GraphicsEnvironment.java:207)
at java.awt.Window.<init>(Window.java:535)
at java.awt.Frame.<init>(Frame.java:420)
at javax.swing.JFrame.<init>(JFrame.java:224)
at example.guitool.snip.SnipFXSwing.doShowSwing(SnipFXSwing.java:231)
...
On to the next approach.
Approach 2 - Initialize AWT before JavaFX
Some internals require, that AWT is initialized before JavaFX. One approach
is this:
private static void startAWTFX(String[] args) {
java.awt.Toolkit.getDefaultToolkit();
Application.launch(SnipFXSwing.class, args);
}
To try that, the SnipFXSwing class is started with the argument awtfx.
Well, as can be seen, nothing is seen. The application hangs and
it must be terminated.
A system property is needed:
private static void startAWTFX(String[] args) {
System.setProperty("javafx.macosx.embedded", "true");
java.awt.Toolkit.getDefaultToolkit();
Application.launch(SnipFXSwing.class, args);
}
Ah! It works :-)
Can we see a Swing window please?
Finally some cannons for the sparrows:
Approach 3 - Embed JavaFX in Swing
If for some reason, the javafx.macosx.embedded property cannot be set,
another solution is required.
Lets embed JavaFX in Swing so we can embed Swing in JavaFX.
Starting the application:
private static void startSwFX(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
try {
final JFrame frame = new JFrame("Snippet JavaFX with Swing");
final JFXPanel jfxPanel = new JFXPanel();
frame.getContentPane().add(jfxPanel);
frame.setSize(600, 400);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
Platform.runLater(new Runnable() {
@Override
public void run() {
SnipFXSwing ui = new SnipFXSwing();
try {
ui.start(jfxPanel, frame);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
First Swing is started by creating a JFrame. Such things must be done in the
EDT, so it is wrapped in SwingUtilities.invokeLater. Basically a JFrame
is created and a JFXPanel is added to the frame's content pane.
Now the JavaFX components are created. Similar to Swing with EDT that must be done in
the "JavaFX Application Thread". Hence the Platform.runLater call. A different start
method is used now, as there is no Stage instance:
public void start(JFXPanel jfxPanel, JFrame frame) throws Exception {
this.mainFrame = frame;
Scene mainScene = createMainScene();
frame.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
if (mainFrame!=null) {
Platform.runLater(new Runnable() {
@Override
public void run() {
doExit();
}
});
} else {
super.windowClosing(e);
}
}
});
jfxPanel.setScene(mainScene);
}
Similar to the Stage related start method, the Scene is created, however, it
is set in the JFXPanel instance. Also the code to handle a closed main window
must be slightly different. Instead of a Stage, a JFrame must be observed, which
is done with a WindowListener. A little tricky is the call to doExit. As this
shall be JavaFX code, it must be run in the JavaFX Application Thread, so again
Platform.runLater is used.
The "JavaFX in JFXPanel" main window:
Pressing the "Swing" button:
(*Wiping sweat away...*)
Resources