Freitag, 15. März 2013

JavaFX and Swing (on a Mac)

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:

  1. Ability to show Swing components
  2. Provide a guitool based on JavaFX like the one based on Swing
  3. 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

1 Kommentar: