Custom JavaFX controls and Scene Builder

by Cuchaz

This is my second blog post today! I've never written two posts in one day before, so this is kind of exciting. I must be inspired or something. The other post today was about using JavaFX to build GUIs for cross-platform games. This time though, I'm going to write about a tip to make your life easier if you want to write custom JavaFX controls and use Scene Builder at the same time.

Scene Builder takes much of the drudgery out of building GUIs and I really like it a lot. It's even useful when you don't just want to build an entire scene, but want to make some small reusable GUI component instead, like a single control.

I'll assume you already know JavaFX and Scene Builder well enough to make a window/view/scene/whatever for your app. Here's the extra stuff you'll need to make reusable controls.

First, make the FXML file for your control in Scene Builder. Here's an example:

The world's most egotistical custom control.

Building a control is about the same as building a scene. If you're making a control though, be sure to check the 'fx:root' box and leave the 'controller' text field empty. Build everything else like you normally would. The choice of root node doesn't matter in the slightest, but remember what you picked.

Then write the controller class for the control. Make sure your control class extends the class of the root node you picked in your scene.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package myapp.view;

import java.io.IOException;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.CheckBox;
import javafx.scene.layout.AnchorPane;

public class MyControl extends AnchorPane {
	
	@FXML private CheckBox answer;

	public MyControl()
	throws IOException {
		
		// attach FXML to this control instance
		FXMLLoader loader = new FXMLLoader(getClass().getResource("MyControl.fxml"));
		loader.setRoot(this);
		loader.setController(this);
		loader.load();
	}
	
	@FXML
	public void initialize() {
		
		// listen for checkbox changes
		answer.selectedProperty().addListener((obs, oldVal, newVal) -> {
			System.out.println("Answer changed to: " + (newVal ? "Yes" : "No"));
		});
	}
}

Congratulations! Now you have a custom JavaFX control.

That's it. We're done, right?

Nope.

Scene Builder makes it really easy to create custom JavaFX controls, but it't not very good at letting you use custom JavaFX controls just yet.

Let's imagine a fantastical and completely imaginary scenario where we want to use our custom control in the same app where we created it. This must be an incredibly unlikely scenario somehow, because Scene Builder doesn't really provide any good ways to do this.

If Scene Builder had its way, we'd use one of two options.

1. Import the FXML file into Scene Builder

To do this, just click the little gear icon next to the 'Library' thingy, find the 'Jar/FXML Manager' menu option. Then click the 'Add Library/FXML from file system' link. Pick your control's FXML file and then WHAM! Your control is sitting pretty in the 'custom' section of the controls library. This seems like what we want.

This looks perfect, right?

Now all that's left is to drag our custom control into another FXML file and...

uuhhh...

Scene Builder didn't use our custom control at all! It just embedded a copy of the control's FXML file into the current FXML file.

This is incredibly not useful and mildly frustrating. On to option 2 then.

2. Make a jar of your custom control and import it into Scene Builder manually. Using the GUI. And every time you make a change to your control, you get to do this all over again.

This approach actually works the way you'd want it to, it's just tedious as hell. You have to make a build script that exports just your controls (and all their dependencies) to a jar file that no one's ever going to use except Scene Builder. Sadly, the build script can't re-import the jar file into Scene Builder, so you have to fiddle with the menus after every update too. I won't walk through the process here, but you get the idea. It's a pain in the ass if you're actually developing the control you want to import. The think>build>test cycle is just too long.

Wait a minute, what if we just edit the FXML file manually?

We can just add a <MyControl/> tag to the FXML directly, like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import myapp.view.MyControl?>


<BorderPane prefHeight="200.0" prefWidth="400.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.111">
   <center>
      <AnchorPane prefHeight="75.0" prefWidth="185.0" BorderPane.alignment="CENTER">
         <children>
         	<MyControl/>
         </children>
      </AnchorPane>
   </center>
</BorderPane>

This actually works just fine for your app, but then Scene Builder won't want anything to do with you anymore. If you try to load this FXML file into Scene Builder, it will just crash and say it doesn't know what the hell a 'MyControl' is.

Which means you can never use Scene Builder to edit this FXML file again.

But Scene Builder is the whole reason JavaFX development is so enjoyable. I'm not living without it. The old way of developing GUIs is just too painful.

There has to be an easier way

There is, but it's a bit of a hack.

The root cause of the problem here is that Scene Builder needs your control to be on its classpath to use it. Scene Builder's import menus lets you modify the classpath, but not in terribly useful ways. I think it's using a bunch of chained classloader magic under-the-hood so you can load and unload controls at runtime. It doesn't seem to accept a folder full of compiled classes though, so it just doesn't do what we want in this case.

What we really want to do is mess around with Scene Builder's actual classpath.

Thankfully, there's a way to do just that. Go find your Scene Builder installation folder. Since I'm running Linux, my copy is installed at /opt/SceneBuilder.

Scene Builder runs inside a JVM process, but it looks like that JVM gets launched by a small binary executable, which means we can't fiddle with the launch settings directly. Thankfully, the Scene Builder folks made a backdoor for just such an occasion. Find the app/SceneBuilder.cfg file within your Scene Builder installation directory.

It should look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
[Application]
app.name=SceneBuilder
app.mainjar=dist.jar
app.version=8.3.0
app.preferences.id=com/oracle/javafx/scenebuilder/app
app.mainclass=com/oracle/javafx/scenebuilder/app/SceneBuilderApp
app.classpath=
app.runtime=$APPDIR/runtime
app.identifier=com.oracle.javafx.scenebuilder.app

[JVMOptions]

[JVMUserOptions]

[ArgOptions]

Change that app.classpath= bit to point to a folder where there might be class files for custom controls. Like the bin folder for your app.

7
app.classpath=/path/to/myapp/bin

Re-launch Scene Builder for the FXML file we added our custom <MyControl/> tag to and voilĂ ! Everything works now!

MyControl really is awesome now.

Two caveats though...

1. The classpath trick as I described it above only works for one project.

I mean, this will get your first project out the door just fine. (Unless you're like me and constantly get distracted by other side-projects.) But what about the second project?

Thankfully for us, that config file isn't just asking for a file path. It can handle a whole classpath. Which means we can add multiple projects to it using your favorite classpath separator. Like this:

7
app.classpath=/path/to/myapp1/bin:/path/to/myapp2/bin
On Linux, the separator character is :

Eventually your Scene Builder will accumulate projects in its classpath. Those project references could remain there even after those projects get deleted. The JVM doesn't seem to mind when you specify classpath entries that don't exist though, so the fallout from this isn't terribly bad. You can always just cleanup your Scene Builder classpath additions manually if you like.

2. Scene Builder still won't show your control in the 'custom' controls area.

Aside from messing with the compile-a-jar-with-all-your-controls-in-it-and-all-their-dependencies-in-there-too-every-time-you-make-a-change option, I don't know of a way to fix this. So there's still no clean/easy way to drag a custom control into your scene.

There's a simple workaround though. Just manually type the tag for your custom control into the FXML file. And add the import statement too.

You only have to do this once wnen you add the tag to your scene. After that, you can restart Scene Builder and keep editing like normal. You can even edit the custom control that you added manually.

Now you can develop custom JavaFX controls using Scene Builder much more easily

Caveat #2 means this isn't quite a perfect solution, but it's the best thing I've found so far. And it's certainly much easier than the path Scene Builder would have you go with it's official import menu.

If this tip saves you some hassle and makes your life easier, let me know. It took me a long damned time to figure this all out, so I thought I'd share and hopefully save someone else the trouble. You can send me a tweet or something on Twitter at @cuchaz. There's a shiny Twitter link on the bottom of the post too.

Yup, we have RSS. Subscribe and never miss a post.

I also announce blog posts on Twitter.