Introduction

JavaFX 2.0 is an API and runtime for creating Rich Internet Applications (RIAs). JavaFX was introduced in 2007, and version 2.0 was released in October 2011. One of the advantages of JavaFX 2.0 is that the code can be written in the Java language using mature and familiar tools. This article focuses on using the layout capabilities of JavaFX 2.0 to make nodes in the scene graph appear where you want them to be and be the appropriate size as the window is resized.

JavaFX comes with its own layout classes, shown in Figure 1 (from Amy Fowler’s JavaOne 2011 presentation), whose purpose is to provide convenient ways to lay out the user interface regardless of the size of the scene or type of platform. These classes are located in the javafx.scene.layout package. This article contains an example of using the BorderPane and HBox classes, and other layout-related JavaFX classes, to create a commonly used UI layout format.

Figure 1: JavaFX Layout Classes

All layout classes are responsible for controlling the location of their managed child nodes (a node can optionally be set as not managed). In addition, most layout classes under certain circumstances also resize their resizable child nodes. In Step 1, we’ll discuss the behavior of resizable nodes further.

Going from left to right on the bottom of Figure 1, Table 1 contains a brief description of each layout class and its behavior pertaining to resizing its children.

Table 1: Layout Classes and Their Children-Resizing Behavior

Layout Class

Description and Children-Resizing Behavior

AnchorPane

Allows child node edges to be anchored to the parent’s edges. Children are not resized.

BorderPane

Provides classic top, left, right, bottom, center child placement. Horizontally resizes child nodes placed in the top and bottom, vertically resizes child nodes placed in the leftand right, and resizes nodes placed in the center both horizontally and vertically. All resizing is up to the node’s maximum size in the relevant directions.

StackPane

Layers children back to front. Resizes child nodes to fill the size of the parent (up to each node’s maximum width and height).

HBox

Contains a single row of nodes. Child nodes are resized horizontally to their preferred widths, but individual child nodes can be explicitly set to grow horizontally to their maximum widths. By default, child nodes are resized vertically to their maximum heights.

VBox

Contains a single column of nodes. Child nodes are resized vertically to their preferred heights, but individual child nodes can be explicitly set to grow vertically to their maximum heights. By default, child nodes are resized horizontally to their maximum widths.

TilePane

Provides a horizontal or vertical flow of uniform “tiles” that wrap. Resizes child nodes to fill the size of the tile (up to the node’s maximum width and height).

FlowPane

Provides horizontal or vertical flow of children that wrap. Children are not resized.

GridPane

Places children in a flexible grid, which is great for complex layout. Resizing is based upon constraints explicitly set on a given row or column.

We’ll discuss the behavior of resizable nodes a bit later in Step 1.

Overview of the LayoutSansTearsSolution Application

To help you learn how to lay out the UI in JavaFX, an example application named LayoutSansTearsSolution will be employed. As shown in Figure 2, the UI for this application contains a header area, a tabbed pane area, and a footer area, and its appearance is adjusted, as the user would expect, when the window is resized.

The LayoutSansTearsExerciseproject that you'll download in the next section contains starter code for this example application. In its current form, the application’s runtime appearance looks like Figure 6. During the course of this article, you'll modify the code to implement the layout appearance of the LayoutSansTearsSolution application shown in Figure 2.

Figure 2: Screen Capture of the LayoutSansTearsSolution Application

When you resize the window of the LayoutSansTearsSolution application, as shown in Figure 3, the relative positions of the nodes are adjusted accordingly. For example, the Footer Left and Footer Right labels move closer to each other, and the App Title header text stays horizontally centered.

Figure 3: Screen Capture of LayoutSansTearsSolution after Resizing the Window

In addition, the tab pane in the center of the window resizes to take all available horizontal space, as well as any space not used by the header and footer.

Obtaining and Running the LayoutSansTearsExercise Project

Expand the LayoutSansTearsExercise project into a directory of your choice.

Start NetBeans, and select File -> Open Project.

From the Open Project dialog box, navigate to your chosen directory and open the LayoutSansTearsExercise project, as shown in Figure 4. If you receive a message stating that the jfxrt.jar file can't be found, click the Resolve button and navigate to the rt/lib folder subordinate to where you installed the JavaFX 2.0 SDK.

To run the application, click the Run Project icon on the toolbar or press the F6 key. The Run Project icon looks like a Play button on a media (for example, DVD) player, as shown in Figure 5.

Figure 5: Running theApplication in NetBeans

The LayoutSansTearsExercise application should appear in a window, as shown in Figure 6.

Figure 6: Screen Capture of the LayoutSansTearsExercise Application

Notice that the App Title text, search text area, and Go button are bunched together on the right side of the header. Also, the search text area appears stretched vertically but our desired height is just tall enough to allow a couple of lines of text entry. In addition, the Footer Left and Footer Right labels are bunched together on the left side of the footer.

Your mission will be to add code that implements the layout appearance and behavior described previously and shown in Figures 2 and 3. Next, we’ll discuss the steps you can follow to implement this appearance and behavior.

Step 1: Reviewing the Layout Strategy Used in the LayoutSansTearsExercise Application

The top-level layout strategy used in the LayoutSansTearsExercise application is to place a BorderPane in the root of the scene. When a layout class, or any other resizable class, is used as the root node of the scene, that node is automatically resized as the scene is resized. Consequently, as the user resizes the application window (the JavaFX stage), the BorderPane is resized to fill all the space inside the window.

In addition to creating a BorderPane, an HBox is created to hold the nodes in the header, and it is placed in the top of the BorderPane. A TabPane is then created and placed in the center of the BorderPane. Another HBox is then created to hold the nodes in the footer, and it is placed in the bottom of the BorderPane.

Take a look at the code in Listing 1, which shows the starter code for the example application and is located in the LayoutSansTears.javafile.

package javafxpert.layoutsanstears.ui;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import javafx.stage.Stage;
public class LayoutSansTears extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage primaryStage) {
Region headerLeftSpring = new Region();
// TO DO: Declare a variable of type Region, assigning a
// new Region object to it for use as a "spring" in the
// header between the title of the app and the search box
// TO DO: Declare a variable of type Region, assigning a
// new Region object to it for use as a "spring" in the
// footer between the left and right labels
ImageView logo = new ImageView(
new Image(getClass().getResourceAsStream("images/javafx-logo.png"))
);
HBox searchBox = HBoxBuilder.create()
.spacing(5)
.children(
TextAreaBuilder.create()
.prefWidth(120)
.prefHeight(40)
// TO DO: Use a method of the TextAreaBuilder to set the maximum
// height of the TextArea to its preferred size
.build(),
ButtonBuilder.create()
.text("Go")
.build()
)
.build();
Scene scene = SceneBuilder.create()
.stylesheets("javafxpert/layoutsanstears/ui/myStyles.css")
.width(800)
.height(500)
.root(
BorderPaneBuilder.create()
.top(
HBoxBuilder.create()
.children(
logo,
headerLeftSpring,
LabelBuilder.create()
.id("app-title")
.text("App Title")
.build(),
// TO DO: Insert the Region object created to act as a "spring"
searchBox
)
.build()
)
.center(
TabPaneBuilder.create()
.tabs(
TabBuilder.create()
.text("Tab A")
.build(),
TabBuilder.create()
.text("Tab B")
.build(),
TabBuilder.create()
.text("Tab C")
.build()
)
.build()
)
.bottom(
HBoxBuilder.create()
.id("footer")
.children(
new Label("Footer Left"),
// TO DO: Insert the Region object created to act as a "spring"
new Label("Footer Right")
)
.build()
)
.build()
)
.build();
HBox.setHgrow(headerLeftSpring, Priority.ALWAYS);
// TO DO: Use a static method of HBox to allow the headerRightSpring
// to compete for any extra horizontal space
// TO DO: Use a static method of HBox to allow the footerCenterSpring
// to compete for any extra horizontal space
// TO DO: Use a static method of HBox to give the searchBox
// a margin of 20 pixels on the top and 10 pixels on the right
primaryStage.setTitle("Layout Sans Tears: Exercise");
primaryStage.setScene(scene);
primaryStage.show();
}
}

Listing 1: LayoutSansTears.java

Using the SceneBuilder and BorderPaneBuilder Classes

The starter code in LayoutSansTears.java makes use of builder classes in the JavaFX 2.0 API, including the SceneBuilderand BorderPaneBuilderclasses shown in Listing 2. These classes create instances of Scene and BorderPane, respectively.

Note: There are some “TO DO” comments in the code (for you to fill in later during Step 2) that have been omitted from Listing 2.

In Listing2, the HBoxBuilder class is employed to create a horizontal container in which the nodes for the header are placed. These nodes, as shown in Listing 1, are as follows:

The JavaFX logo, which is the ImageView shown in Listing 1

A “spring” node that we’ll examine later in Step 2

The application title

A search box that comprises the TextArea and Button shown in Listing 1

The HBoxBuilder class is also used in Listing 2 to create a horizontal container in which the Footer Left and Footer Right labels for the footer are placed.

There are many builder classes in the JavaFX API, and their purpose is to enable a declarative-style of programming to create and set the properties of objects. For example, as you just experienced, the SceneBuilder class in Listing 2 creates an instance of the Scene class, and it populates that instance with properties such as the desired width and height of the scene. As you saw in Listing 1, other builder classes used in this application are HBoxBuilder, TextAreaBuilder, ButtonBuilder, BorderPaneBuilder, LabelBuilder,TabPaneBuilder, and TabBuilder.

Note: Although builder classes were used in this example, the application could have been written without them in a more procedural style or expressed in FXML. The "See Also" section at the end of this article contains a link you can visit to learn about FXML.

Understanding the Behavior of Resizable Nodes

All the nodes in LayoutSansTears.java are resizable by a parent layout container. When a node is resizable, its parent will resize it during layout, usually to its preferred size. By the same token, applications never set the size of a resizable node directly. All the Node subclasses in JavaFX are resizable by a parent layout container with the exception of Text, ImageView, Group, and subclasses of Shape. In order for the size of these non-resizable classes to change, the application must set the sizes directly (except for Group, whose size assumes the collective bounds of its children).

Let’s examine the behavior of the LayoutSansTears.java application (in the LayoutSansTearsExercise project) in light of your understanding of resizable node behavior and the layout behavior described in Table 1.

For example, when the user resizes the stage vertically, the BorderPane is resized because it is assigned to the root of the scene. As the BorderPane is resized vertically, it does not attempt to vertically resize its top and bottom nodes, so they retain their preferred heights. Therefore, the node in the center of the BorderPane (which is the TabPane) is resized up to its maximum height. The question you may be asking is, “How much can the TabPane grow vertically?” Or, stated differently, “What is the maximum height of a TabPane?” To answer that question, an understanding of clamped versus unbounded nodes is necessary, which we’ll discuss next.

Understanding Clamped Versus Unbounded Nodes

A clamped node is a resizable node whose maximum height is the same as its preferred height, and its maximum width is the same as its preferred width. For example, as described in Table 1, when an HBox attempts to resize the height of a child node, it can resize it only up to its maximum height. Consequently, a clamped node can be resized only up to its preferred width and height.

An unbounded node is a resizable node whose maxWidth() and maxHeight() return Double.MAX_VALUE. Consequently, there are no limits on the width and height to which a layout container may resize an unbounded node.

A resizable node can be clamped in one direction and unbounded in another. For example, by default, a MenuBar has an unbounded width and a clamped height. This allows it to be resized horizontally as wide as the layout container requires, while retaining its preferred height. Table 2 contains the default clamped and unbounded characteristics of the resizable classes.

The answer to the question, “What is the maximum height of a TabPane?” is that its maximum height is unbounded. Take a moment to experiment more with resizing the application window, validating that the positions and sizes of the nodes shown in Figure 6 are consistent with your understanding.

For example, the preferred height of the TextArea in the upper right side of the UI is set to 40 pixels, yet its appearance is much taller than that. This is because the TextArea is unbounded by default, so the HBox in Listing 3 resizes TextArea to the height of the HBox:

As intimated in Listing 3, we’re going to put you to work addressing the “TO DO” comments. The goal is to fine-tune the layout from the LayoutSansTearsExercise appearance shown in Figure 6 to the LayoutSansTearsSolution appearance shown in Figure 3.

Overriding the Default Maximum Size of a Resizable Node

To make the height of the TextArea in Listing 3 clamped, rather than unbounded, we need to set its maximum height to its preferred height. The recommended way to do this is to use a constant defined in the Region class named USE_PREF_SIZE.

To implement this in our example, go ahead and add the following method invocation to the TextAreaBuilder where indicated in Listing 3:

.maxHeight(Region.USE_PREF_SIZE)

When you run the application, the TextArea should now appear with its preferred height, as previously shown in Figure 2.

If you subsequently wanted to reset the maximum height of the TextArea (or any other resizable node) back to its default, you’d pass the Region.USE_COMPUTED_SIZE constant as an argument into the setMaxHeight() method of the TextArea.

Table 3 contains the methods of a resizable node that you can use to make its width, height, or both width and height clamped, unbounded, or reset.

Table 3: Making a Resizable Node Clamped, Unbounded, or Reset to its Default

Clamped

Unbounded

Reset to Default

setMaxWidth()

Region.USE_PREF_SIZE

Double.MAX_VALUE

Region.USE_COMPUTED_SIZE

setMaxHeight()

Region.USE_PREF_SIZE

Double.MAX_VALUE

Region.USE_COMPUTED_SIZE

setMaxSize()

Region.USE_PREF_SIZE, Region.USE_PREF_SIZE

Double.MAX_VALUE, Double.MAX_VALUE

Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE

Another way of making a TextArea appear at its preferred height in an HBox is to set the fillHeight property of the HBox to false. This allows all the child nodes to be their preferred heights. This approach would work in our example program, but note that it applies to all the child nodes in the HBox, not to an individual node as the HBox.maxHeight() method does.

Now that you’ve addressed the height issue of the TextArea shown in Figure 6, let’s tackle the issue in which nodes in the header are bunched up on the right side.

It looks like we’re going to put you to work again, this time inserting a horizontal "spring" into the HBox so that more of its children can spread out. As shown in Listing 4, we’ve already inserted such a spring between the logo and the label, which is why they are able to spread out as shown in Figure 6.

To implement the spring, we first declare a variable named headerLeftSpring of type Region and assign a new Region object to it. As shown in Listing 4, to enable the headerLeftSpring node to grow horizontally in the HBox, we used the static setHgrow() method of HBox. Passing the Priority.ALWAYS argument indicates that we want the node to take all the available horizontal space, sharing it with other nodes whose horizontal grow constraint is ALWAYS. Other constants in the Priority enum are SOMETIMES and NEVER, which can be used to further control growing behavior.

Go ahead and implement a spring between the title of the application and the search box by inserting code where indicated in the previous listings. Then run the application to see that the App Title label is centered horizontally in the header due to the fact that the two springs are sharing any available horizontal space. The appearance of the application should be similar to Figure 7.

Figure 7: LayoutSansTearsExercise with Step 2 Partially Completed

By the way, you can use the technique described above to place a vertical spring into a VBox by using the static setVgrow() method of VBox. In addition, the GridPane class has static setHgrow() and setVgrow() methods for use with the GridPane layout.

Now that you’ve addressed the horizontal spacing issues in the header shown in Figure 6, let’s give the search box a margin so that it doesn’t touch the upper right corner.

Setting Margins Around Individual Child Nodes of a Layout Container

The search box shown in Figure 6 is implemented by the code in Listing 5.

There are a couple of ways that our desired padding of 20 pixels on the top and 10 pixels on the right can be implemented in the search box. One way to achieve this in our example application would be to add the padding() method invocation to the HBoxBuilder that creates the search box, as shown in the following snippet:

Another more generally applicable way to implement a margin around a child node of a layout container is to use the static setMargin() method of its layout class. To implement this in our example, go ahead and add the following line where indicated in Listing 5.

HBox.setMargin(searchBox, new Insets(20, 10, 0, 0));

When you run the application, the search box should now be inset 20 pixels from the top and 10 pixels from the right, as previously shown in Figure 2.

Now that you’ve addressed all the spacing issues in the header, we’ll leverage JavaFX CSS to make the footer have the appearance shown in Figure 2.

Step 3: Leveraging JavaFX CSS to Modify the Layout

A very powerful aspect of JavaFX is the ability to use CSS to dynamically style the nodes, including layout container nodes, in a scene. The footer area shown most recently in Figure 7 is implemented by the code shown in Listing 6 (which is a snippet from Listing 1) as well as the CSS document shown in Listing 7.

Using CSS Style Sheet Properties to Modify the Layout

As shown in Listing 6, a style sheet is associated with our JavaFX application by using the stylesheets() method of the SceneBuilder class.

The HBox that contains the labels in the footer leverages the style properties and values from the #footer selector in Listing 7 through the use of the id() method in the HBoxBuilder. Because of this, the HBox is rendered with the border color and width specified in the #footer selector, as shown most recently in Figure 7.

As demonstrated previously, there are multiple ways to put margins around child nodes in a layout container. The technique we’ll use for the footer involves modifying the style sheet by adding the following line in the #footer selector where indicated in Listing 7:

-fx-padding: 5;

The –fx-padding property causes the interior of the layout container to be padded in its interior by 5 pixels on its top, right, bottom, and left sides. Alternatively, four space-separated values can be supplied if you want these padding values to be different from each other.

Go ahead and implement the padding modification, and while you’re at it, take care of the one remaining loose end: implementing a spring between the two labels in the footer. Just follow the same approach as when you implemented a spring in the header.

Running the LayoutSansTearsExercise project should now result in the same appearance as running the LayoutSansTearsSolution project shown in Figure 2.

Conclusion

JavaFX has very powerful features for laying out a user interface, some of which we’ve discussed in this article and demonstrated in the LayoutSansTearsSolution application. These features enable your applications to appear the way you want them to appear, regardless of the size of the scene or the type of platform. Understanding the behavior of each type of layout class, as well as concepts such as clamped versus unbounded nodes, will go a long way toward helping you make the UI appear exactly the way you want it.

See Also

About the Author

Jim Weaver is an independent Java and JavaFX developer, author, and speaker with a passion for helping rich-client Java and JavaFX become preferred technologies for new application development.

Books that Jim has authored include Inside Java, Beginning J2EE, and Pro JavaFX Platform, with the latter being updated to cover JavaFX 2.0. His professional background includes 15 years as a Systems Architect at EDS and the same number of years as an independent developer and software development company owner. Jim is an international speaker at software technology conferences, including presenting at the JavaOne 2011 conferences in San Francisco and São Paulo and keynoting at the O’Reilly OSCON/Java 2011 conference in Portland, Oregon.

Jim blogs at http://javafxpert.com, tweets @javafxpert, and can be reached at jim.weaver[at]javafxpert.com.