Way, way back in 2014 when I started learning JavaFX, I tried using Scene Builder and FXML and found it to be an amazing waste of time and effort. One of the best decisions I ever made was to abandon it early on, and just code screens by hand. I advise everyone else to do the same.
There seems to be some confusion as to whether you can even hand code a screen in JavaFX, not to mention how you would go about doing it. This is probably because virtually all of the beginners’ tutorials available make the assumption that “drag and drop” is only method that’s going to be palatable to new learners.
In this article I’m going to build a complete screen, bound to a data model, that accepts input and works. It will be fully configured and styled. You’ll see how just applying some of the basic concepts of Clean Coding will guide the design to produce an amazing amount of functionality in a small amount of code that’s takes very little time to write.
You’ll see that the end result is simple and easy to read and understand, and should be equally easy to maintain and expand.
We are going to build an application from the start. So we’ll begin with the usual main class that extends
Application, and it will look something like this:
Now, most of that is boilerplate, and the only thing you really need to work out to actually build the screen is to create
sceneRoot. I’ve defined it as a
Region, and that’s a good starting place, because anything that extends Region is going to work as the root of a
Scene. Examples of
StackPane as well as
HBox. Your region can be any one of those, plus a few others I haven’t listed.
One thing that we’ll get back to later is the stylesheet. It’s good practice to put your styling into a stylesheet and then use selectors to apply it to various nodes in your application. I have a “default” stylesheet I use for a lot of projects, so I just went and added it to the
A general rule of thumb is that you should only extend classes that you are going to add new functionality to. If you’re just configuring them, then you should use a builder class - which is what we are doing here. Personally, I like to use the
Builder interface, because it’s built-in, obvious and works well. That’s what the
ScreenBuilder class is, it’s an implementation of
So let’s get to that.
The ScreenBuilder Class
We’ll start with just the bare essentials, to get to something that will compile and run as quickly as possible. We want to be able to see our screen as we build it out, so it needs to run right from the get-go.
Because it’s an excellent all-round start for a screen that has a basic structure, I’ve chosen to use a
It looks like this:
Not pretty, but it’s a start. The key point is that now you can see what you’ve built and exactly how it will look on the screen. In about 6 lines of code!
Some Notes About Code Structure and Organization
I’ve started this out the way that I would structure most screen builders. The
build() method instantiates the container - in this case a BorderPane - and then each of the sections is built in its own method. These methods all return something generic, like a Node or a Region, and not the actual class of the object being returned. This is for two reasons:
- The calling method should never reach inside the returned value to fiddle with it. All of the configuration should go inside that creation method.
- It’s easier to change it later, or to wrap another container around the original returned value. We’ll see this in action later on.
By the same token, the creation method should never attempt to reach up to the container to tinker with the layout.
Adding Some Styling to the Heading
To illustrate how the design grows, and where the code belongs, I wrapped the Text in a
HBox with a border and made the text bigger. When that was done, it was clear that everything was cramped together too much. The Text was too close to the border, and the border was too close to the edge of the screen. So some space was added. The code now looks like this:
There are some things you should notice:
- The creation method for the heading has now been renamed to something more descriptive.
- The creation method for the heading now returns an HBox, instead of a Text, yet the signature of the method did not need to be changed.
- The styling for the Text and the HBox are done in the creation method.
- The internal spacing for the HBox (the padding) is done in the creation method.
- The external spacing for the heading Node is done in the
build()method, as this is an element of the layout of the
Just to be really, really clear about this. No way, no how, should the line:
go inside the
createHeadingBox() method has no knowledge that it’s return value is going to be placed into a
BorderPane. So it cannot do this.
Also, since the
createHeadingBox() method returns a Node, there’s no way that
build() can call
setPadding() on it.
Layout vs Configuration
An important concept when writing clean JavaFX code is to understand the difference between “Layout” and “Configuration”.
Configuration is the setting of properties and behaviours of a Node. This can include things like styling, scaling, setting minimum or maximum height and width, visibility, and binding to properties in a Model.
Layout is the population of a Region with child Nodes and determining their position and bounds, as well as sizing parameters in relation to other elements of the layout.
It’s possible, even probable, that a Region will be defined in your code with both Configuration and Layout. We have that so far in our example. The
createHeadingBox() method configures the styling for an HBox and then defines it’s layout.
You will find that Configuration tends to be less specific to a particular screen than layout. For instance, you’ll create a styling for Text that works in a particular context, and then you’ll want to re-use that styling throughout a screen, or throughout an application, or even across multiple applications. Once again, this is an important point, as we’ll see in a little bit.
Adding A Data Entry Section
We’re going bring these ideas together now by adding a
GridPane with some prompts and data entry fields. It’s going to go in the centre of the
Now it looks like this:
Let’s look at what’s been done here. First, a new call was added to the main layout section to populate the Centre region of the
BorderPane through the
That new method creates a
GridPane with two columns, and then populates the left column of the
GridPane with prompt
Texts, and the right column with
Notice that the Configuration has been completely split away from the Layout. It might be argued that most of the “configuration” of a
GridPane has to do with defining how the layout works, but the important thing is that contents of the
GridPane and their positioning in the
GridPane are isolated from everything else. That makes it easy to see how the layout works. To that end, all of the configuration of the
TextFields is handled by separate methods.
I’ve also added a “test-border” styling to the
GridPane that will be removed later. One of the things that can be challenging with JavaFX is understanding which region owns the whitespace on the screen. Putting a temporary border around regions is a great way to understand what’s really going on.
Using the DRY Principle
There is a lot of boilerplate code in JavaFX.
The key to writing good JavaFX code is to aggressively apply the DRY (Don’t Repeat Yourself) Principle whenever you can. The layout of that
GridPane would be agonizing to read if the boilerplate for the
Texts was repeated inside code, like this:
That’s going to add up to 8 extra lines of code in a method that only has 10 lines to start with! Not to mention another 8 lines for the
TextFields if they’re all defined in-line as well.
This concept is carried over to the creation of the
ColumnConstraints for the
GridPane as well. It’s just two lines of code each time, but the single line of code that’s used instead has the advantage of having the method name describing what the
ColumnConstraints are for, so it’s even easier to read.
Adding Some More Data Entry
We’re going to add a TextArea to the screen, to show how the screen design grows and the code is morphed to accommodate it. Rather than repeat the whole class, I’ll just show the changed code:
Now it looks like this:
createDataEntrySection() was renamed to
createGridPane() and a new
createDataEntrySection() was written. Essentially, this “pushes” the
GridPane down one level in the container structure, as it’s now contained in a new HBox which comprises the
BorderPane's centre region.
The “BioBox” area is just a
VBox with a
Text and a
TextArea. There’s no particular configuration to the VBox, so there no need to even instantiate it into a variable. Just create it and return it. In my opinion, the layout is so simple that there’s no need to split it apart from the configuration of the
Adding a “Save” Button and Linking to the Outside World
We have this “data entry” section, but the data isn’t going anywhere. So we need to add a Model to get the information out of the screen. This article isn’t about MVC, so I won’t go into much detail about this, but you should be able to see how it works.
Here’s a Model:
I’ve put some default values into the properties of the Model so that we can see how the populated data looks on the screen without having to type it in each time.
I’m not going to do anything with it other than bindings, so I haven’t included any getters or setters for the fields, just the “…Property()” methods. It’s going to be passed to the ScreenBuilder class in its constructor along with a
Runnable to handle the “Save” action (which isn’t going to actually do anything in this example):
Now, lets link up the Model and add the Button:
Which looks like this:
As you can see, we’ve now updated
createInputField() to take a
StringProperty as a parameter, and it’s now bound to the
Text property of the
TextField. Back up in
createGridPane(), it’s easy to see how each
TextField is bound to a Model property.
Button is wrapped in an
HBox so that it can be set up at the right-hand side of the screen. You could use an
AnchorPane if you want to, and then attach it to the right side. The
setOnAction() method of the
Button just runs the Runnable.
One Last Refinement
The last 4 methods in
ScreenBuilder don’t really have anything to do with our entry screen at all. They’re just generic methods to configure nodes that could appear on any screen in our application. So, let’s make them static and move them to a new class,
ScreenBuilder class only has about 70 lines of executable code, it’s easy to read, and it’s easy to understand:
This article ended up being more about code style than JavaFX, but that doesn’t really surprise me. There’s nothing magical about JavaFX, it’s just Java.
It took me much longer to write the text of this article than it did to write the code.
I wrote it and modified the code as I wrote the article, so I can’t say for sure, but my guess is that the whole finished product would have taken about one cup of coffee for me to complete from scratch. Even less if I had used my own library of configuration methods and classes (a bit like the
Widgets class we just created).
But yet it’s not really a trivial screen. It does something. Add a proper Controller and an adapter to an external API or persistence layer and it’s a fully functional application.
As you can see, building a screen by hand in JavaFX is fairly simple.
Create a container, put some stuff in it and run it.
Put some more stuff inside.
If the new stuff needs a layout that your current container can’t supply, then put your new stuff into its own container, and then add that to your original container.
Keep on adding stuff to the containers, creating new containers as necessary until you’re done. Keep running it over and over to see what the changes look like.
I hope this was helpful. I’ve seen quite a few people express confusion about how to start writing JavaFX code by hand, and there doesn’t seem to be anything out there (until now) to show them how.
Let me know if this helps you, or if you have more questions.
Creating a screen is just one part of building an application, and you’ll need to apply a framework to get to the finish line. If you want to use Model-View-Controller with JavaFX, you can carry on with this article here.