FX My Life IV: Navigating With Style

Blake
15 min readApr 4, 2021

Ultimately, our toy application should have a bunch of tiles on the front that we can click on which then launch particular games and what-have-yous (where “what-have-yous” are our experiments).

To get a sense of this, let’s create a new SceneBuilder project called “main.fxml” and put a FlowPanel down as the first component. From there, we’ll add an ImageView component and set its width and height to 64 each. Create a simple graphic as a placeholder — mine is called “filler.png” — and set the Image field of the ImageView to that.

Now make three copies of that ImageView (copy and paste will work) and play with the FlowPanel’s size to see how things resize. If you make the panel wide enough, you’ll get a single row:

Behold my mighty artistic skills!

If you narrow things down a bit, you’ll get both columns and rows:

And you can of course narrow it so much that it’s all in one column.

Now, you may notice that my background is green. To get that effect, you can set the FlowPanel’s Style property to GREEN or any other color JavaFX understands. (I think you can also use RGB codes, but we’ll look into that much later.)

Styling the FlowPanel

I did this for a reason: We’re going to put the Main FlowPanel into the center area of the BorderPane we created earlier. It should fill the center area completely, no matter what size the window is changed to.

Navigating Nodes

JavaFX scenes (the collection of components that are displayed to the user) are created from graphs, which is a tree-like data structure. From the Oracle docs:

It’s like a family tree, only you have one parent.

What we did last time was set up the Root Node, and with Screen Controller, created a way to swap the root in and out. Now, what we want to able do, though, is swap out the center “branch node” — showing the Flow Panel we created above in some cases, and showing the actual game in others.

By the way, it’s certainly possible to package up your components and install them into Scene Builder, as we’ll see later when we incorporate other people’s components, and later still, when we incorporate our own. But that’s a bit of a digression at this point.

We could just load our Flow Panel like we did our buttons previously, like so:

@Override
public void start(Stage primaryStage) throws Exception{
primaryStage.setTitle("Fun 'n' Games with JavaFX");
Scene scene = new Scene(new Group(), 800, 600);
scene.getStylesheets().add(getClass().getResource("styles.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();

screenController = new ScreenController(scene); //not using, currently
screenController.addScreen("sample", FXMLLoader.load(getClass().getResource( "sample.fxml" )));
switchScreen("sample");

scene.setRoot(FXMLLoader.load(getClass().getResource( "main.fxml" )));
}

This is pretty dumb, though, since it will just write over everything Screen Controller does, and will give us this:

Buttons Be Gone

But we want to replace just the center node of the Border Pane, not the entire Border Pane! Well, we can just change the last line of code like so:

((BorderPane) scene.getRoot()).setCenter(FXMLLoader.load(getClass().getResource( "main.fxml" )));

Typecasting in Java is one of the ugliest constructs I’ve ever seen. Nonetheless, this works, and gives us:

New AND improved!

Well, this works right up until we want to swap out that center pane for a game field. If we resize the main window, it looks like the Flow Pane is growing with it, but this is an illusion. Don’t be fooled! Note that while the green fills up the screen as you expand the window, when you shrink the window, the images never reflow into columns.

So, what we have is a really big Flow Pane. We want the Flow Pane to be the size of its parent.

Whose Size Is It Anyway?

Figuring out how to make things size accordingly in JavaFX is not as intuitive as one would like. This is in part due to the complexities of making components and their containers cohabit peacefully, but whatever the reason, it’s something we have to tackle.

The basics of sizing elements in JavaFX seem to boil down to three pairs of values: MinWidth/MinHeight, MaxWidth/MaxHeight and PrefWidth/PrefHeight. The Min and Max sets are pretty straightforward: They are the lines you do not cross. The component mayn’t/mustn’t get any smaller (or bigger, respectively). The Prefs are, supposedly, what you’d like to see the dimensions of the item be, but I’m not sure how that works in practice. In a desktop app, resolution would seem to be the key factor in ideal size. In a mobile/tablet app — resolution would still seem to be key. So, maybe you’re supposed to change the Pref based on resolution?

This is an area where the minimal descriptions offered by the docs and books don’t shed a lot of light into the thinking that went into something.

Experimenting with these elements is somewhat enlightening, and somewhat…not. The Max values seem to generally work: The component will never get larger than MaxHeight or MaxWidth. The Min values, on the other hand, don’t seem to have any effect. That is, if you set MaxWidth to 512, the component won’t get wider than 512, but if you set MinWidth to 128, it might never get smaller than 128, but you won’t know because it won’t get much smaller than 256.

Since we can assume this isn’t broken, it suggests some other constraint is keeping the component large. This is borne out by the fact that in the Scene Builder, you can shrink the flow pane down so it’s one long row (or column); there’s nothing inherent in the flow pane that says it can’t get small.

But we are putting the FlowPane into a BorderPane, and it doubtless has its own ideas about constraints. Indeed, if we go to our sample.fxml file and set the Pref values there to 128, we get a funny looking thing:

But we also will get the ability to size the Flow Pane down to two columns. If we set the Flow Pane’s Min values to 256, then that will constrain the Pane from being shrunk smaller than four columns.

So it does all make sense, after a fashion, even if it does feel like a power struggle, it comes down to the parent respecting its children’s wishes to the best of its ability and as long as they don’t contradict the parent’s own settings.

Kind of like life.

Under Control

Let’s play tic-tac-toe! It’s so…boring. But it’s trivial and since our focus is going to be navigating back-and-forth, it’s a fine thing to start with. I made a crude little graphic to represent the game and set it to the first ImageView in our app:

Tic-Tac-Toe: The Foundation of Every Great Gaming Empire

I turned the Cursor property to Open Hand, as well, so when the program runs, it looks like:

The red circle is to call out the cursor change. You won’t see it locally.

Now, for the first time, we want to actually do something with our app, or rather, have the user do something. When he clicks on the tic-tac-toe, we’ll want to present the appropriate board for him.

Let’s start by having a screen to navigate to, to wit, a board on which tic-tac-toe might be played. There are many ways we can set this up, but I’m going to put down a Flow Pane with a 3x3 grid on the left and a 1x2 grid on the right. The 3x3 for placing the Xs and Os and the 1x2 for grabbing the Xs and Os, as if they were pieces to physically place down on the board.

A world of opportunities. Well, nine. Nine opportunities.

I may abandon this approach later, but it will serve for now. I made the grid lines visible on both grids; otherwise, we’d just see a blank window, which leaves open the possibility we haven’t done what we thought we’d done.

JavaFX is set up to handle UI interface actions through descendants of the Controller class. We actually get a “free” one when we create our JavaFX app in IntelliJ that looks like this:

package sample;

public class Controller {

}

We will actually use this class for our application-wide events, but right now our interest is in making our tile window do stuff. Now, the Scene Builder can’t really do anything as far as generating code, but if we create a file called TileController and give it a method for handling clicks:

package sample;

import javafx.scene.input.MouseEvent;

public class TileController {

public void openTTT(MouseEvent mouseEvent) {
System.out.println("Opening Tic-Tac-Toe");
}

}

Scene Builder will recognize it. The format for handling mouse events, unsurprisingly is:

public void functionName(MouseEvent mouseEvent) {}

This is where a book you paid $40 for would take a page or two to list all the properties of MouseEvent, which you would scan and then forget. Just assume you can get whatever information you need from a MouseEvent about the event for now, and we’ll actually roll out examples for doing specific tasks when the time comes. Then the info has a chance of sticking.

Right now, we’re just wiring things up, as we say in the professional world, so we’re satisfied to print out that we’ve made the connection. If you look at the lower-left side of the Scene Builder, you’ll see a section labeled Controller. This allows us to attach a controller to any component. A controller can be used across the entire app or as narrowly as a single component. (I like to start general, and narrow down as needed.)

If you open that section, you’ll see a drop-down for setting the Controller class for the current component:

Clicking on that will reveal all the classes you can use. Our new TileController class should be in there. When I did this, the Controller class IntelliJ made for me did not show up. From my experiments, this seems to be because Scene Builder does not regard empty classes as worthy of being selected here.

However, the main function did come up. So there’s no requirement to use a Controller at all, apparently. Certain advantages and disadvantages to using the Controller class suggest themselves at this point, but let’s plow on and see what issues actually arise from the approaches we use.

The other thing you can see in the spreadsheet is that I’ve given the Tic-Tac-Toe “tile” (my crude rendering of a tic-tac-toe board) an fx:id value of tilettt. This is done in the code panel of the Object Inspector:

If you never actually explicitly reference the fx:id in your code, you don’t need it. (And if you don’t need it, it’s best to leave it out.) Since we’re experimenting, we don’t really know yet if we’re going to need it. We do know we’re going to need connect a click on the tic-tac-toe tile with our existing event, and we can do that also in the code pane, by scrolling down a bit:

The code pane is just loaded with possible event handlers categorized by whether they’re keyboard, mouse, swipe, touch, etc. Scroll down to the “On Mouse Clicked” event and click on the drop down arrow. Scene Builder will present a list of methods in your controller (TileController, recall), regardless of whether the call is legitimate.

I confess I don’t quite understand this but if our Tile Controller was defined as:

public class TileController {

public void openTTT(MouseEvent mouseEvent) {
System.out.println("Opening Tic-Tac-Toe");
}

public void open2(MouseEvent mouseEvent) {
System.out.println("Opening 2");
}

public void whatever() {
System.out.println("What?!");
}

}

Not only will “open2” appear as an option to attach to a MouseEvent, but so will “whatever”:

What’s more, it’ll actually work! I hooked up the On Mouse Click of the second tile to the “whatever” function and then ran the app. When I clicked on it, the console showed “What?!”, with no errors.

I don’t know if this is a useful feature or a horrible oversight, but it’s good to know.

Presto! Change-O!

Well, let’s set aside that and actually swap out the tile form for the tic-tac-toe form. What we want to do is conceptually pretty simple: We want to take the contents of the Border Pane’s central area (currently showing our tiled view) and replace it with the tic-tac-toe board.

//load tic-tac-toe board
//delete central pane contents
//put tic-tac-toe into central pane

Loading the board is no problem.

Node node = FXMLLoader.load(getClass().getResource("tttgrid.fxml"));

But now what? We need access to that center pane, which we don’t currently have. We could, however, take the following steps:

In the Main object, we could float up the “bp” variable we created in previous chapters:

public BorderPane bp;
. . .
bp = ((BorderPane) scene.getRoot());
bp.setCenter(FXMLLoader.load(getClass().getResource( "main.fxml" )));

So, now the Border Pane is exposed. But how to get to the actual Main object? Well, one trick is to add a me variable to Main:

public static Main me;

Note that it’s a class variable. In start, set the me variable:

@Override
public void start(Stage primaryStage) throws Exception{
me = this;

Now we can replace the tile pane with the tic-tac-toe board easily:

import sample.Main;
. . .
public void openTTT(MouseEvent mouseEvent) throws IOException {
System.out.println("Opening Tic-Tac-Toe");

Node node = FXMLLoader.load(getClass().getResource("tttgrid.fxml"));
Main.me.bp.setCenter(node);
}

Now when we click on the tic-tac-toe tile, the window switches instantly:

Exposing the Main class and instance works in the case of singletons, like the main window of an app, but it has something of a smell to it. We won’t worry about this just yet because have a number of other things we want to do which may well remove this smell as a side-effect. But first, let’s make the window switch a little more stylish.

Sliding Into Your DMs Like…

Animations are potentially incredibly complicated, and in the books I’ve seen, they’ll show a few, then present massive lists of all the kinds of animations that can be done — sort of like Mouse Event fields on steroids — which I immediately brain dump. Much like Mouse Event, just assume that any kind of animation can be done and work from there as you find it useful.

In this case, I want to switch screens from the main tile to the tic-tac-toe window, but with panache. Let’s slide that puppy in from the right.

This gives us a very basic animation that underlies the main premise behind animations generally: An animation is the gradual change of one value toward another. In our case, we’ll add the tic-tac-toe on the right side of the window and gradually change its X value to 0, so that it slides left and seats itself in the upper-left corner of the center pane.

Node node = FXMLLoader.load(getClass().getResource("tttgrid.fxml")); //1
Pane owner = (Pane) Main.me.bp.getCenter(); //2
owner.getChildren().add(node); //3
node.translateXProperty().set(owner.getWidth()); //4
KeyValue kv = new KeyValue(node.translateXProperty(), 0, Interpolator.EASE_IN); //5
KeyFrame kf = new KeyFrame(Duration.seconds(0.25), kv); //6
Timeline timeline = new Timeline(); //7
timeline.getKeyFrames().add(kf); //8
timeline.setOnFinished(event -> { //9
Main.me.bp.setCenter(node);
});
timeline.play(); //10

We have to:

  1. Create the new node, as before
  2. Get the owner, on which the sliding action will take place
  3. Add the node to the owner: This is the thing that makes the tic-tac-toe board visible at all
  4. Now, set the starting X value for the node to the width of the owner, i.e., the far right side of the owner.
  5. Set the key values, which will determine the algorithm for the animation. (EASE_IN means to start slow and accelerate.)
  6. The key frames determine the “stops” along the way for the animation. So by the EASE_IN algorithm, the key frames figure out how to get the X from the start value to zero in 1/4 of a second.
  7. Create a timeline for the animation to occur on
  8. Add the keyframes to the timeline.
  9. Determine what happens when the key frame is finished: I’m just setting the center pane to the node which gets rid of whatever’s there already.
  10. Let ‘er rip.

This is a good little set up for playing with animation. Looking at it, two seconds is a very long animation, and even 1/2 second may quickly grow tiresome for a user. You can doubtless figure out how sliding in from the left, the top or the bottom would work with this.

But I can already tell this isn’t going to be work that I want to repeat for every new idea I want to throw into this app, so let’s take a tip from the Screen Controller and make a similar thing that replaces only specific nodes in the app.

Node Controller

We know we’re going to want to swap in all kinds of things in the center panel, but we may also want to swap things in and out of the top panel or, who knows, maybe the side and bottom panels as well.

Let’s start by using the Controller class IntelliJ built for us when we created our JavaFX app. The Controller class is created when the JavaFX application is created and, when all the JavaFX pieces are in place, the Controller’s initialize class is invoked.

In our Controller’s constructor, we’ll load up all the FXMLs. Then we initialize it. It is the Controller which ends up with access to the fx:ids created in its FXML, so we can add a simple pane to the central area of our Border Pane, then get access to it in the code by assigning it an fx:id of “central” and using the @FXML directive.

@FXML
public Pane central;
public NodeController nodeController;
public static Controller me;

We’ll load our nodes in the constructor:

public Controller() {
me = this;
nodeController = new NodeController();
loadNode("tic-tac-toe", "tttgrid.fxml");
loadNode("main", "main.fxml");
}

loadNode itself will be a simple helper function that prints out an error when it can’t load an FXML:

public void loadNode(String name, String resource) {
try {
nodeController.addNode(name, FXMLLoader.load(getClass().getResource(resource)));
} catch(Exception e) {
System.out.println(e.getMessage());
}
}

When all the FX startup code has fired, the Controller’s initialize will be called and we can set the central panel to our tile window, main:

//@FXML or @Override, no longer necessasry
public void initialize() {
NodeController.me.activate("main", central);
}

In times past, you needed to annotate your initialize method with @FXML or declare that it implemented Initializable and use an @Override directive. This is no longer needed: FXML just looks for any method initialize with no arguments and calls it.

Node Controller will be slightly more complex than Screen Controller was, both because I’ve moved the animation code there, and also because it’s going to keep a hash-map of nodes that can be “activated” by name:

public class NodeController {

public static NodeController me;
private final HashMap<String, Parent> nodeMap = new HashMap<>();

public NodeController() {
me = this;
}

protected void addNode(String name, Parent node) {
nodeMap.put(name, node);
}

protected void removeNode(String name) {
nodeMap.remove(name);
}

I’m doing the cheesy me/this gag again so that I can call our application’s Node Controller from Tile Controller, as shown above:

NodeController.me.activate("main", central);

I’m not too concerned because it actually eliminates Tile Controller needing access into Main, so that seems like progress.

Node Controller’s activate is the workhorse:

protected void activate(String name, Pane owner) {
Node node = nodeMap.get(name);
node.translateXProperty().set(owner.getWidth());
owner.getChildren().remove(node);
owner.getChildren().add(node);

KeyValue kv = new KeyValue(node.translateXProperty(), 0, Interpolator.EASE_IN);
KeyFrame kf = new KeyFrame(Duration.seconds(0.25), kv);

Timeline timeline = new Timeline();
timeline.getKeyFrames().add(kf);
timeline.setOnFinished(event -> {
owner.getChildren().setAll(node); //odd, perhaps, to setAll after adding above, but the point is we want to remove anything there without caring what IS there
});
timeline.play();
}

Same code as we went over previously: The first section fetches the node (which was previously loaded by Controller and put into Node Controller’s hashmap via addNode), the second section sets up the animation, and the third section executes the animation.

It’s easy to see how we could make the activate do any number of animations, specified as a third parameter.

Since we’re using Node Controller to set up the central area, we no longer need to do that in Main.

@Override
public void start(Stage primaryStage) throws Exception{
me = this;
primaryStage.setTitle("Fun 'n' Games with JavaFX");
Scene scene = new Scene(new Group(), 800, 600);
scene.getStylesheets().add(getClass().getResource("styles.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();

screenController = new ScreenController(scene); //not using, currently
screenController.addScreen("sample", FXMLLoader.load(getClass().getResource( "sample.fxml" )));
switchScreen("sample");

//bp = ((BorderPane) scene.getRoot());
//bp.setCenter(FXMLLoader.load(getClass().getResource( "main.fxml" )));
}

The last two lines are commented out, as you can see. Now, if we run the app, we get:

And if we click on the tic-tac-toe tile, the board slides in from the left and we get:

Nice.

But wait, something changed. If we uncomment those lines in main again that set up the Border Pane:

Which is bigger and more centered. Hmmm. Well, we’ll have to fix that next time.

--

--

Blake
0 Followers

I am a poor, wayfaring stranger, traveling through this world of woe.