JavaFX Documentation Project
JavaFX Documentation Project
Published 2021-12-07
Version unspecified
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1. Contributors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Contributing. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.3. License. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
2. Scene Graph . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.1. Overview . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
2.2. Transformations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.4. Timing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
3. UI Controls. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.1. ChoiceBox . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.2. ComboBox. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.3. ListView. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
3.4. TableView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.5. ImageView. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.6. LineChart . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
3.7. Pagination . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
4. Layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
4.2. StackPane. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
4.4. Clipping . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
4.5. GridPane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
4.8. AnchorPane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
4.9. TilePane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
4.10. TitledPane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
5. CSS. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
6. Performance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
1.1. Contributors
This project was initiated by Jonathan Giles [http://jonathangiles.net], but anyone can contribute. Any questions,
concerns, or feedback should be in the first instance be directed to Jonathan via email
[mailto:[email protected]].
This project would not be possible without the contributors who work hard on the content contained within this
documentation. The authors of this document are:
• Abhinay Agarwal
• Almas Baimagambetov [https://almasb.github.io/]
• Carl Walker [http://bekwam.blogspot.com/]
• Christoph Nahr [http://kynosarges.org/]
• Jonathan Giles [http://jonathangiles.net]
• Gerrit Grunwald [https://harmoniccode.blogspot.com/]
1.2. Contributing
Contributing to this project is easy - fork the GitHub [http://www.github.com/FXDocs/docs] repo, edit the relevant
files, and create a pull request! Once merged, your content will form a part of the documentation and you’ll have
the unending appreciation of the entire community!
The JavaFX Documentation Project uses AsciiDoc as the syntax of choice for writing the documentation. The
AsciiDoc Syntax Quick Reference [http://asciidoctor.org/docs/asciidoc-syntax-quick-reference/] guide is a great resource
for those learning how to write AsciiDoc.
Authors are encouraged to add their name to the contributors list in the previous section.
1.3. License
1
2
Chapter 2. Scene Graph
2.1. Overview
A scene graph is a tree data structure that arranges (and groups) graphical objects for easier logical
representation. It also allows the graphics engine to render the objects in the most efficient way by fully or
partially skipping objects which will not be seen in the final image. The following figure shows an example of
the JavaFX scene graph architecture.
At the very top of the architecture there is a Stage. A stage is a JavaFX representation of a native OS window.
At any given time a stage can have a single Scene attached to it. A scene is a container for the JavaFX scene
graph.
All elements in the JavaFX scene graph are represented as Node objects. There are three types of nodes: root,
branch and leaf. The root node is the only node that does not have a parent and is directly contained by a scene,
which can be seen in the figure above. The difference between a branch and a leaf is that a leaf node does not
have children.
In the scene graph, many properties of a parent node are shared by children nodes. For instance, a transformation
or an event applied to a parent node will also be applied recursively to its children. As such, a complex hierarchy
of nodes can be viewed as a single node to simplify the programming model. We will explore transformations
and events in later sections.
An example of a "Hello World" scene graph can be seen in the figure below.
3
Figure 2. Hello World Scene Graph
One possible implementation that will produce a scene graph matching the figure above is as follows.
HelloApp.class
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 300, 300));
stage.show();
}
4
Figure 3. Hello World
Important notes:
2.2. Transformations
We will use the following app as an example to demonstrate the 3 most common transformations.
5
TransformApp.class
transform(box);
@Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 300, 300, Color.GRAY));
stage.show();
}
In JavaFX, a simple transformation can happen in one of the 3 axes: X, Y or Z. The example application is in
2D, so we will only consider X and Y axes.
6
2.2.1. Translate
In JavaFX and computer graphics, translate means move. We can translate our box by 100 pixels in X axis
and 200 pixels in Y axis.
Figure 5. Translate
2.2.2. Scale
You can apply scaling to make a node larger or smaller. Scaling value is a ratio. By default, a node has a scaling
value of 1 (100%) in each axis. We can enlarge our box by applying scaling of 1.5 in X and Y axes.
box.setScaleX(1.5);
box.setScaleY(1.5);
}
7
Figure 6. Scale
2.2.3. Rotate
A node’s rotation determines the angle at which the node is rendered. In 2D the only sensible rotation axis is the
Z axis. Let’s rotate the box by 30 degrees.
box.setRotate(30);
}
Figure 7. Rotate
8
2.3. Event Handling
An event notifies that something important has taken place. Events are typically the "primitive" of an event
system (aka event bus). Generally, an event system has the following 3 responsibilities:
The event notification mechanism is done by the JavaFX platform automatically. Hence, we will only consider
how to fire events, listen for events and how to handle them.
UserEvent.class
Since event types are fixed, they are usually created within the same source file as the event. We can see that
there are 2 specific types of events: LOGIN_SUCCEEDED and LOGIN_FAILED. We can listen for such specific
types of events:
9
Finally, we are able to construct and fire our own events:
For example, LOGIN_SUCCEEDED or LOGIN_FAILED could be fired when a user attempts to log in to an app.
Depending on the login result we can allow the user access the app or lock him out of the app. Whilst the same
functionality can be achieved with a simple if statement, there is one significant advantage of an event system.
Event systems were designed to enable communication between various modules (sub-systems) in an application
without tightly coupling them. As such, a sound might be played by an audio system when the user logs in. Thus,
maintaining all audio related code in its own module. However, we will not delve deeper into architectural styles.
The object e above is of type MouseEvent and can be queried to obtain various information about the event,
e.g. x and y positions, number of clicks, etc. Finally, we can do the same with keys:
The object e here is of type KeyEvent and it carries information about the key code, which can then be
mapped to a real physical key on the keyboard.
2.4. Timing
It is important to understand the timing difference between the creation of JavaFX UI controls and the display of
the controls. When creating the UI controls — either through direct API object creation or through
FXML — you may be missing certain screen geometry values such as the dimensions of a window. That is
available later, at the instant when the screen is shown to the user. That showing event, called OnShown, is the
time at which a window has been allocated and the final layout computations completed.
To demonstrate this, consider the following program which displays the screen dimensions while the UI controls
10
are being created and the screen dimensions when the screen is shown. The following screenshot shows the
running of the program. When the UI controls are being created (new VBox(), new Scene(),
primaryStage.setScene()), there are no actual window height and width values available as evidenced by the
undefined "NaN" values.
However, the values for width and height are available once the window is shown. The program registers an
event handler for the OnShown event and prepares the same output.
StartVsShownJavaFXApp.class
@Override
public void start(Stage primaryStage) throws Exception {
11
);
primaryStage.setScene( scene );
Sometimes, you’ll know the screen dimensions in advance and can use those values at any point in the JavaFX
program. This includes before the OnShown event. However, if your initialization sequence contains logic that
needs these values, you’ll need to work with the OnShown event. A use case might be working with the last
saved dimensions or dimensions based on program input.
12
Chapter 3. UI Controls
3.1. ChoiceBox
This article demonstrates the ChoiceBox. The ChoiceBox control is a list of values from which the user
makes a selection. In this particular implementation, there is an empty value which makes the selection optional.
The following screenshot shows ChoiceBox app. A selection "Furniture" is made and the Save Button is
pressed. The Save Button call invokes a println() which prints out the object.
The program puts a Label, a ChoiceBox, and a Button into an HBox. An action is set on the Save Button
which prints out the value.
The simplest usage of the ChoiceBox is to fill it with Strings. This ChoiceBox in this article is built on a
JavaFX class called Pair. Pair is a general container for any key/value pair and can be used in place of a
domain or other special-purpose object. Strings should only be used if they can be used without manipulation or
decoded consistently.
13
ChoicesApp.class
@Override
public void start(Stage primaryStage) throws Exception {
initChoice();
saveButton.setOnAction(
(evt) -> System.out.println("saving " + assetClass.getValue())
);
primaryStage.setTitle("ChoicesApp");
primaryStage.setScene( scene );
primaryStage.show();
}
3.1.1. StringConverter
When using a complex object to back a ChoiceBox, a StringConverter is needed. This object serializes a
String to and from the ChoiceBox. For this program, only the toString() needs to be coded which replaces the
default toString() of the Pair object. (Both toString and fromString will need an implementation in order to
compile.)
An empty object EMPTY_PAIR is used to prevent NullPointerExceptions. The returned value from
assetClass().getValue() can be accessed and compared consistently without adding special null handling logic.
14
private final static Pair<String, String> EMPTY_PAIR = new Pair<>("", "");
@Override
public Pair<String, String> fromString(String string) {
return null;
}
});
assetClass.getItems().add( EMPTY_PAIR );
assetClass.getItems().addAll( assetClasses );
assetClass.setValue( EMPTY_PAIR );
}
The ChoiceBox is used to select from a list of values. When the list of values is a complex type, provide a
StringFormatter to serialize a list object into something presentable. If possible, use an empty object (rather than
a null) to support optional values.
Complete Code
ChoicesApp.class
@Override
public void start(Stage primaryStage) throws Exception {
15
label,
assetClass,
saveButton);
hbox.setSpacing( 10.0d );
hbox.setAlignment(Pos.CENTER );
hbox.setPadding( new Insets(40) );
initChoice();
saveButton.setOnAction(
(evt) -> System.out.println("saving " + assetClass.getValue())
);
primaryStage.setTitle("ChoicesApp");
primaryStage.setScene( scene );
primaryStage.show();
}
@Override
public Pair<String, String> fromString(String string) {
return null;
}
});
assetClass.getItems().add( EMPTY_PAIR );
assetClass.getItems().addAll( assetClasses );
assetClass.setValue( EMPTY_PAIR );
}
16
3.2. ComboBox
A ComboBox is a hybrid control that presents a list of values plus an edit control. This article demonstrates a
basic form of the ComboBox which is an un-editable list of items built on a complex data structure.
This screenshot shows a ComboBoxApp containing a list of expense accounts. The accounts are stored in a
key/value JavaFX class Pair. The console shows the result of a save operation after the user selects "Auto
Expense".
This code adds a Label, a ComboBox, and a Button to an HBox. The ComboBox is instantiated as a field and
initialized in a method presented later initCombo(). A handler is put on the Save Button which outputs a value if
an item is selected or a special message if no item is selected.
17
CombosApp.class
@Override
public void start(Stage primaryStage) throws Exception {
initCombo();
primaryStage.setTitle("CombosApp");
primaryStage.setScene( scene );
primaryStage.show();
}
3.2.1. CellFactory
The initCombo() method adds several expense accounts to a List. This List is added to the ComboBox items
after an empty Pair object is added. The initial value is set to the EMPTY_PAIR which is a constant.
If not specified, the ComboBox will use the toString() method of the object (in this article, a Pair) to render a
backing object. For Strings, such as a "Yes" or "No" selection, no extra code is needed. However, the toString()
of a Pair will output both the human-readable key and the machine-preferred value. The requirements for this
ComboBox are to use only the human-readable keys in the display.
To do this, a cellFactory is provided which will configure a ListCell object with the Pair key as the
contents. The Callback type is verbose, but the gist of the factory is set the text of a ListCell in the
18
updateItem() method of an anonymous inner class. Notice that the super class method must be called.
account.getItems().add( EMPTY_PAIR );
account.getItems().addAll( accounts );
account.setValue( EMPTY_PAIR );
Callback<ListView<Pair<String,String>>, ListCell<Pair<String,String>>>
factory =
(lv) ->
new ListCell<Pair<String,String>>() {
@Override
protected void updateItem(Pair<String, String> item,
boolean empty) {
super.updateItem(item, empty);
if( empty ) {
setText("");
} else {
setText( item.getKey() );
}
}
};
account.setCellFactory( factory );
account.setButtonCell( factory.call( null ) );
}
The Callback is used in the setButtonCell() method to provide a cell for the editing control. Note that this
program is not editable which is the default. However, the factory.call(null) is needed otherwise only the contents
of the popup menu will be properly formatted and the view of the control at rest will fallback on a toString().
This article presented a simple usage of ComboBox. Since this control was not editable, ChoiceBox can be
substituted. For un-editable graphical renderings (ex a color coded shape for a status value), ComboBox still
would be needed to define the specific Node used in the control.
Complete Code
CombosApp.class
19
private final static Pair<String, String> EMPTY_PAIR = new Pair<>("", "");
@Override
public void start(Stage primaryStage) throws Exception {
initCombo();
primaryStage.setTitle("CombosApp");
primaryStage.setScene( scene );
primaryStage.show();
}
account.getItems().add( EMPTY_PAIR );
account.getItems().addAll( accounts );
account.setValue( EMPTY_PAIR );
Callback<ListView<Pair<String,String>>, ListCell<Pair<String,String>>>
factory =
(lv) ->
20
new ListCell<Pair<String,String>>() {
@Override
protected void updateItem(Pair<String, String> item,
boolean empty) {
super.updateItem(item, empty);
if( empty ) {
setText("");
} else {
setText( item.getKey() );
}
}
};
account.setCellFactory( factory );
account.setButtonCell( factory.call( null ) );
}
3.3. ListView
Binding is used heavily to keep the data structures in sync with what the user has selected.
This screenshot shows the Application which contains a top row of ToggleButtons which set the filter and a
ListView containing the objects.
21
Figure 11. Screenshot of ListView Filtering App
The complete code — a single .java file — is listed at the end of the article.
Data Structures
The program begins with a domain model Player and an array of Player objects.
22
The Player class contains a pair of fields, team and playerName. A toString() is provided so that when the object
is added to the ListView (presented later), a custom ListCell class is not needed.
The test data for this example is a list of American baseball players.
Model
As mentioned at the start of the article, the ListView filtering is centered around the management of two lists. All
the objects are stored in a wrapped ObservableList playersProperty and the objects that are currently viewable
are stored in a wrapped FilteredList, viewablePlayersProperty. viewablePlayersProperty is built off of
playersProperty so updates made to players that meet the FilteredList criteria will also be made to
viewablePlayers.
ReadOnlyObjectProperty<ObservableList<Player>> playersProperty =
new SimpleObjectProperty<>(FXCollections.observableArrayList());
ReadOnlyObjectProperty<FilteredList<Player>> viewablePlayersProperty =
new SimpleObjectProperty<FilteredList<Player>>(
new FilteredList<>(playersProperty.get()
));
23
Filtering Action
A handler is attached the ToggleButtons which will modify filterProperty. Each ToggleButton is supplied a
Predicate in the userData field. toggleHandler uses this supplied Predicate when setting the filter property. This
code sets the special case "Show All" ToggleButton.
@SuppressWarnings("unchecked")
EventHandler<ActionEvent> toggleHandler = (event) -> {
ToggleButton tb = (ToggleButton)event.getSource();
Predicate<Player> filter = (Predicate<Player>)tb.getUserData();
filterProperty.set( filter );
};
The ToggleButtons that filter a specific team are created at runtime based on the Players array. This Stream does
the following.
hbox.getChildren().add( tbShowAll );
hbox.getChildren().addAll( tbs );
ListView
The next step creates the ListView and binds the ListView to the viewablePlayersProperty. This enables the
ListView to receive updates based on the changing filter.
24
ListView<Player> lv = new ListView<>();
lv.itemsProperty().bind( viewablePlayersProperty );
The remainder of the program creates a Scene and shows the Stage. onShown loads the data set into the
playersProperty and the viewablePlayersProperty lists. Although both lists are in sync in this partcular version of
the program, if the stock filter is every different than "no filter", this code would not need to be modified.
vbox.getChildren().addAll( hbox, lv );
primaryStage.setScene( scene );
primaryStage.setOnShown((evt) -> {
playersProperty.get().addAll( players );
});
primaryStage.show();
This article used binding to tie a list of viewable Player objects to a ListView. The viewable Players were
updated when a ToggleButton is selected. The selection applied a filter to a full set of Players which was
maintained separately as a FilteredList (thanks @kleopatra_jx). Binding was used to keep the UI in sync and to
allow for a separation of concerns in the design.
Further Reading
To see how such a design would implement basic add and remove functionality, visit the following page
https://courses.bekwam.net/public_tutorials/bkcourse_filterlistapp.php.
Complete Code
@Override
public void start(Stage primaryStage) throws Exception {
//
// Test data
//
Player[] players = {new Player("BOS", "David Ortiz"),
new Player("BOS", "Jackie Bradley Jr."),
new Player("BOS", "Xander Bogarts"),
new Player("BOS", "Mookie Betts"),
new Player("HOU", "Jose Altuve"),
new Player("HOU", "Will Harris"),
new Player("WSH", "Max Scherzer"),
new Player("WSH", "Bryce Harper"),
new Player("WSH", "Daniel Murphy"),
new Player("WSH", "Wilson Ramos") };
25
//
// Set up the model which is two lists of Players and a filter criteria
//
ReadOnlyObjectProperty<ObservableList<Player>> playersProperty =
new SimpleObjectProperty<>(FXCollections.observableArrayList());
ReadOnlyObjectProperty<FilteredList<Player>> viewablePlayersProperty =
new SimpleObjectProperty<FilteredList<Player>>(
new FilteredList<>(playersProperty.get()
));
//
// Build the UI
//
VBox vbox = new VBox();
vbox.setPadding( new Insets(10));
vbox.setSpacing(4);
//
// The toggleHandler action wills set the filter based on the TB selected
//
@SuppressWarnings("unchecked")
EventHandler<ActionEvent> toggleHandler = (event) -> {
ToggleButton tb = (ToggleButton)event.getSource();
Predicate<Player> filter = (Predicate<Player>)tb.getUserData();
filterProperty.set( filter );
};
//
// Create a distinct list of teams from the Player objects, then create
// ToggleButtons
//
List<ToggleButton> tbs = Arrays.asList( players)
.stream()
.map( (p) -> p.getTeam() )
26
.distinct()
.map( (team) -> {
ToggleButton tb = new ToggleButton( team );
tb.setToggleGroup( filterTG );
tb.setOnAction( toggleHandler );
tb.setUserData( (Predicate<Player>) (Player p) ->
team.equals(p.getTeam()) );
return tb;
})
.collect(Collectors.toList());
hbox.getChildren().add( tbShowAll );
hbox.getChildren().addAll( tbs );
//
// Create a ListView bound to the viewablePlayers property
//
ListView<Player> lv = new ListView<>();
lv.itemsProperty().bind( viewablePlayersProperty );
vbox.getChildren().addAll( hbox, lv );
primaryStage.setScene( scene );
primaryStage.setOnShown((evt) -> {
playersProperty.get().addAll( players );
});
primaryStage.show();
}
27
@Override
public String toString() { return playerName + " (" + team + ")"; }
}
}
3.4. TableView
For JavaFX business applications, the TableView is an essential control. Use a TableView when you need
to present multiple records in a flat row/column structure. This example shows the basic elements of a
TableView and demonstrates the power of the component when JavaFX Binding is applied.
The demonstration app is a TableView and a pair of Buttons. The TableView has four TableColumns: SKU,
Item, Price, Tax. The TableView shows three objects in three rows: Mechanical Keyboard, Product Docs, O-
Rings. The following screenshot shows the app immediately after startup.
The disabled logic of the Buttons is based on the selections in the TableView. Initially, no items are selected
so both Buttons are disabled. If any item is selected — the first item in the following screenshot — the
Inventory Button is enabled. The Tax Button is also enabled although that requires consulting the Tax value.
28
Figure 13. With Taxable Item Selected
If the Tax value for the selected item is false, then the Tax Button will be disabled. This screenshot shows the
second item selected. The Inventory Button is enabled but the Tax Button is not.
29
Item.java
The TableView and TableColumn use generics in their declarations. For TableView, the type parameter
is Item. For the TableColumns, the type parameters are Item and the field type. The constructor of
TableColumn accepts a column name. In this example, the column names diverge slightly from the actual
field names.
TableSelectApp.java
tblItems.getColumns().addAll(
colSKU, colDescr, colPrice, colTaxable
);
30
Adding model items to the TableView is done by adding items to the underlying collection.
TableSelectApp.java
tblItems.getItems().addAll(
new Item("KBD-0455892", "Mechanical Keyboard", 100.0f, true),
new Item( "145256", "Product Docs", 0.0f, false ),
new Item( "OR-198975", "O-Ring (100)", 10.0f, true)
);
At this point, the TableView has been configured and test data has been added. However, if you were to view
the program, you would see three empty rows. That is because JavaFX is missing the linkage between the POJO
and the TableColumns. That linkage is added to the TableColumns using a cellValueFactory.
TableSelectApp.java
Viewing the program at this point will display the data in the appropriate columns.
3.4.2. Selection
To retrieve the selected item or items in a TableView, use the separate selectionModel object. Calling
tblItems.getSelectionModel() returns an object that includes a property "selectedItem". This can be retrieved and
used in a method, say to bring up an edit details screen. Alternatively, getSelectionModel() can return a JavaFX
property "selectedItemProperty" for binding expressions.
In the demo app, two Buttons are bound to the selectionModel of the TableView. Without binding, you might
add listeners that examine the selection and make a call like setDisabled() on a Button. Prior to the TableView
selection, you would also need initialization logic to handle the case where there is no selection. The binding
syntax expresses this logic in a declarative statement that can handle both the listener and the initialization in a
single line.
TableSelectApp.java
btnInventory.disableProperty().bind(
tblItems.getSelectionModel().selectedItemProperty().isNull() ①
);
¬ See "Ignoring Warnings for Null Select Binding Expressions" under "Best Practices" to show how to turn off
warning messages when using this construct
The btnInventory disable property will be true if there is no item selected (isNull()). When the screen is first
displayed, no selection is made and the Button is disabled. Once any selection is made, btnInventory is enabled
(disable=false).
the btnCalcTax logic is slightly more complex. btnCalcTax too is disabled when there is no selection. However,
btnCalcTax will also consider the contents of the selectedItem. A composite binding or() is used to join these two
31
conditions. As before, there is an isNull() expression for no selection. The Bindings.select() checks the value of
Item.taxable. A true taxable Item will enable btnCalcTax while a false item will disable the Button.
TableSelectApp.java
btnCalcTax.disableProperty().bind(
tblItems.getSelectionModel().selectedItemProperty().isNull().or(
Bindings.select(
tblItems.getSelectionModel().selectedItemProperty(),
"taxable"
).isEqualTo(false)
)
);
Bindings.select() is the mechanism to extract a field from an object. selectedItemProperty() is the changing
selectedItem and "taxable" is the single-hop path to the taxable field.
This example showed how to set up a TableView based on a POJO. It also featured a pair of powerful binding
expressions that allow you to link related controls without writing extra listeners and initialization code. The
TableView is an indispensable control for the JavaFX business applications developer. It will be the best and
most familiar control for displaying a list of structured items.
TableSelectApp.java
@Override
public void start(Stage primaryStage) throws Exception {
VBox.setVgrow(tblItems, Priority.ALWAYS );
tblItems.getColumns().addAll(
colSKU, colDescr, colPrice, colTaxable
);
32
tblItems.getItems().addAll(
new Item("KBD-0455892", "Mechanical Keyboard", 100.0f, true),
new Item( "145256", "Product Docs", 0.0f, false ),
new Item( "OR-198975", "O-Ring (100)", 10.0f, true)
);
btnInventory.disableProperty().bind(
tblItems.getSelectionModel().selectedItemProperty().isNull()
);
btnCalcTax.disableProperty().bind(
tblItems.getSelectionModel().selectedItemProperty().isNull().or(
Bindings.select(
tblItems.getSelectionModel().selectedItemProperty(),
"taxable"
).isEqualTo(false)
)
);
primaryStage.setTitle("TableSelectApp");
primaryStage.setScene( scene );
primaryStage.setHeight( 376 );
primaryStage.setWidth( 667 );
primaryStage.show();
}
launch(args);
}
}
3.5. ImageView
JavaFX provides the Image and ImageView classes to display BMP, GIF, JPEG, and PNG graphical images.
Image is a class that holds the bytes of the image and optionally scaling information. The Image object is loaded
by a background thread, and the Image class provides methods for interacting with the load operation. The Image
object is used independently of ImageView to create cursors and app icons.
33
ImageView is a JavaFX Node that holds an Image object. ImageView makes an image available throughout the
framework. An ImageView can be added to a container by itself or alongside other UI controls. For example an
image can be added to a Label by setting the graphic property of the Label.
This screenshot shows a TilePane containing four equally-sized tiles. Each tile contains an ImageView of a
keyboard.
The top-left image is displayed using the original image size of 320x240. The top-right image is scaled
proportionally. Since the top-right image is a rectangle and the containing tile is a square, there are gaps on the
top and bottom to maintain the correct ratio when stretching the width.
The lower-left image fills the container completely. However, in making the rectangular image fit the square
34
container, the image is not scaled proportionally and instead strethed in both directions.
The lower-right image fills the container using a zoomed-in version of the image. A square Viewport is created
from a 100x100 Rectangle2D and scaled up proportionally. While the low-quality image is blurry, it is not
deformed.
3.5.1. Image
The Image class provides constructors to build an Image object from the image file dimensions or from a
transformed object. These three constructor calls create the Image objects used in the top-right, bottom-left and
bottom-right tiles, respectively.
ImageApp.java
@Override
public void start(Stage primaryStage) throws Exception {
The String URL passed in to all forms of the Image constructor is relative to the classpath. An absolute URL
such as "https://www.bekwam.com/images/bekwam_logo_hdr_rounded.png" can also be used. Note that the
absolute URLs will not throw an error if their resource is not found.
image2 and image3 specify dimensions, forming a square larger than the rectangle of the original image. image2
will preserve the aspect ratio ("true"). The constructor of image3 does not preserve the aspect ratio and will
appear stretched.
3.5.2. ImageView
ImageView is a Node container that allows the Image object to be used in JavaFX containers and UI controls. In
the top-left image, a short form of ImageView is used which passes in only the image URL. It will honor the
original dimensions and does not require an additional Image object.
ImageApp.java
iv4.setPreserveRatio(true);
iv4.setFitHeight(360);
iv4.setFitWidth(360);
Rectangle2D viewportRect = new Rectangle2D(20, 50, 100, 100);
iv4.setViewport(viewportRect);
35
iv3 and iv3 are based on the image2 and image3 objects. Recall that these objects produced transformed images
that fit the square container.
iv4 is also based on a transformed Image object, but in the case of iv4, the transformation is done through the
ImageView object rather than the Image. ImageView.setFitHeight is called rather than Image.setFitHeight.
Additionally, the Viewport of iv4 is adjusted. The Viewport controls the visible part of the ImageView. In this
case, the Viewport is defined as a 100x100 section of the Image shifted left 20 pixels and up 50 pixels.
This section demonstrated the Image and ImageView classes which are used to display an image in a container or
other UI control. These classes define the scaling behavior of the image and can be used with a Rectangle2D
Viewport to give additional image display customization.
3.5.3. Source
The complete source code and Gradle project can be found at the link below.
3.6. LineChart
While you can plot a graph using a Line on a Canvas, JavaFX’s LineChart makes graphing easier. In
addition to customizing standard charting components like axis legends, LineChart encapsulates the source data
of the graph. As with all JavaFX controls, LineChart enables you to style the graph using CSS.
This screenshot shows a plot of seven points. The X-Axis has units of Time Constants ranging from 0 to 5. The
Y-Axis shows Voltage ranging from 0 to 1 with more frequent gradients than the X-Axis.
36
3.6.1. Data
LineChart includes an API for managing data. Data points are grouped into series. This particular example uses a
single series.
LineChartApp.java
@Override
public void start(Stage primaryStage) throws Exception {
Each data point is an XYChart.Data object that is added to an XYChart.Series container. To show a
comparison of different series, create additional XYChart.Series objects. These will be rendered as different
colors by the LineChart.
3.6.2. Chart
The LineChart object is created with Axis objects. The first Axis parameter is for the X axis. Each Axis object
includes an optional label: Time Constant, Voltage (Vs). The next two numeric parameters give the lower and
upper bounds. The final parameter sets the step increment. Another form of the LineChart constructor, not used
in this example, accepts the data. This example, makes an explicit add() call on the LineChart’s data field.
LineChartApp.java
lc.getData().add( series );
The LineChart can be customized with a title using setTitle() and an individual style with setStyle(). For
consistency, it is best to use a style sheet so that a single style definition can be applied across a set of
LineCharts.
LineChartApp.java
lc.setTitle("RC Charging");
lc.setStyle("-fx-background-color: lightgray");
There are many other properties that can be set to configure the LineChart. setLegendVisible() removes a series
identifier since there is only one series in this graph. setCreateSymbols() removes a graphic on each data point
37
that was being clipped at the origin and end of the graph.
LineChartApp.java
lc.setCreateSymbols(false);
lc.setLegendVisible(false);
For modest reporting requirements, JavaFX provides classes like LineChart to plot multiple series of data points
into a graph. The LineChart object is highly customizable, giving control over the legends, lines, and data point
icons. Additionally, CSS styling is available to make a set of these reports consistent.
3.6.3. Source
The complete source code and Gradle project can be found at the link below.
3.7. Pagination
Pagination is a UI control that lets you step through blocks of results using next, previous, and direct indexing
buttons. The Pagination class can break up long lists when scrolling is not desired. This section presents a special
case of single-item pages to form a slideshow.
38
Figure 17. Pagination on First of Three Pages
The program begins by defining a array of three JavaFX Images: imageURLs. In the start() method, a Pagination
object is created that references the size of the array. A PageFactory is provided which creates a Node based on
the pageIndex parameter. For this example, the pageIndex is an index into the imageURLs array.
39
SlideShowApp.java
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene( scene );
primaryStage.show();
}
The Pagination class is a simple control to iterate through a long list of items. This example used a single item
per page to form a slideshow. In both cases, this is an alternative to scrolling and is useful when you want the UI
to be fixed in position.
40
Chapter 4. Layout
4.1. VBox and HBox
Layout in JavaFX begins with selecting the right container controls. The two layout controls I use most often are
VBox and HBox. VBox is a container that arranges its children in a vertical stack. HBox arranges its children in
a horizontal row. The power of these two controls comes from wrapping them and setting a few key properties:
alignment, hgrow, and vgrow.
This article will demonstrate these controls through a sample project. A mockup of the project shows a UI with
the following:
• A row of top controls containing a Refresh Button and a Sign Out Hyperlink,
• A TableView that will grow to take up the extra vertical space, and
• A Close Button.
The UI also features a Separator which divides the top part of the screen with what may become a standard
lower panel (Save Button, Cancel Button, etc) for the application.
4.1.1. Structure
A VBox is the outermost container "vbox". This will be the Parent provided to the Scene. Simply putting UI
controls in this VBox will allow the controls — most notably the TableView — to stretch to fit the
available horizontal space. The top controls, the Refresh Button and the Sign Out Hyperlink, are wrapped
in an HBox. Similarly, I wrap the bottom Close Button in an HBox, allowing for additional Buttons.
41
VBox vbox = new VBox();
bottomControls.getChildren().add( btnClose );
vbox.getChildren().addAll(
topControls,
tblCustomers,
sep,
bottomControls
);
This picture shows the mockup broken down by container. The parent VBox is the outermost blue rectangle. The
HBoxes are the inner rectangles (red and green).
42
Figure 19. Mockup Broken Down
Alignment is the property that tells a container where to position a control. topControls sets alignment to the
BOTTOM_LEFT. topRightControls sets alignment to the BOTTOM_RIGHT. "BOTTOM" makes sure that the
baseline of the text "Refresh" matches the baseline of the text "Sign Out".
In order to make the Sign Out Hyperlink move to the right when the screen gets wider, Priority.ALWAYS
is needed. This is a cue to the JavaFX to widen topRightControls. Otherwise, topControls will keep the space
and topRightControls will appear to the left. Sign Out Hyperlink still would be right-aligned but in a
narrower container.
Notice that setHgrow() is a static method and neither invoked on the topControls HBox nor on itself,
topRightControls. This is a facet of the JavaFX API that can be confusing because most of the API sets
properties via setters on objects.
topControls.setAlignment( Pos.BOTTOM_LEFT );
HBox.setHgrow(topRightControls, Priority.ALWAYS );
topRightControls.setAlignment( Pos.BOTTOM_RIGHT );
43
Close Button is wrapped in an HBox and positioned using the BOTTOM_RIGHT priority.
bottomControls.setAlignment(Pos.BOTTOM_RIGHT );
4.1.3. Vgrow
Since the outermost container is VBox, the child TableView will expand to take up extra horizontal space
when the window is widened. However, vertically resizing the window will produce a gap at the bottom of the
screen. The VBox does not automatically resize any of its children. As with the topRightControls HBox, a grow
indicator can be set. In the case of the HBox, this was a horizontal resizing instruction setHgrow(). For the
TableView container VBox, this will be setVgrow().
4.1.4. Margin
There are a few ways to space out UI controls. This article uses the margin property on several of the containers
to add whitespace around the controls. These are set individually rather than using a spacing on the VBox so that
the Separator will span the entire width.
The Insets used by tblCustomers omits any top spacing to keep the spacing even. JavaFX does not consolidate
whitespace as in web design. If the top Inset were set to 10.0d for the TableView, the distance between the top
controls and the TableView would be twice as wide as the distance between any of the other controls.
This picture shows the application when run in its initial 800x600 size.
44
Figure 20. Screenshot
This image shows the application resized to a smaller height and width.
45
at hand. This article presented the two most versatile containers: VBox and HBox. By setting properties like
alignment, hgrow, and vgrow, you can build incredibly complex layouts through nesting. These are the
containers I use the most and often are the only containers that I need.
@Override
public void start(Stage primaryStage) throws Exception {
46
HBox topRightControls = new HBox();
HBox.setHgrow(topRightControls, Priority.ALWAYS );
topRightControls.setAlignment( Pos.BOTTOM_RIGHT );
Hyperlink signOutLink = new Hyperlink("Sign Out");
topRightControls.getChildren().add( signOutLink );
bottomControls.getChildren().add( btnClose );
vbox.getChildren().addAll(
topControls,
tblCustomers,
sep,
bottomControls
);
primaryStage.setScene( scene );
primaryStage.setWidth( 800 );
primaryStage.setHeight( 600 );
primaryStage.setTitle("VBox and HBox App");
primaryStage.setOnShown( (evt) -> loadTable(tblCustomers) );
primaryStage.show();
}
47
public static void main(String[] args) {
launch(args);
}
4.2. StackPane
StackPane lays out its children one on top of another. The last added Node is the topmost. By default,
StackPane will align the children using Pos.CENTER, as can be seen in the following image, where the 3
children are (in the order of addition): Rectangle, Circle and Button.
48
Figure 22. StackPane center-aligned
49
public class StackPaneApp extends Application {
@Override
public void start(Stage stage) throws Exception {
StackPane pane = new StackPane(
new Rectangle(200, 100, Color.BLACK),
new Circle(40, Color.RED),
new Button("Hello StackPane")
);
50
Figure 23. StackPane left-aligned
This screenshot shows an About View. The About View contains a Hyperlink in the middle of the screen
"About this App". The About View uses several JavaFX shapes to form a design which is cropped to appear like
a business card.
51
Figure 24. Screenshot of About View in PaneApp
52
This is a screenshot taken after the lower-right Arc was added. This Arc was placed closer to the bottom-right
edge of the Stage. This forces the Pane to stretch to accommodate the expanded contents.
53
Arc largeArc = new Arc(0, 0, 100, 100, 270, 90);
largeArc.setType(ArcType.ROUND);
The lower-right Arc is positioned based on the overall height of the Stage. The 20 subtracted from the height
is the 10 pixel Insets from the VBox (10 for left + 10 for right).
primaryStage.setWidth( 568 );
primaryStage.setHeight( 320 );
The Hyperlink is not placed in the true center of the screen. The layoutX value is based on a divide-by-three
operation that moves it away from the upper-left design.
4.3.5. Z-Order
As mentioned earlier, Pane supports overlapping children. This picture shows the About View with depth added
to the upper-left design. The smaller Arcs and Circle hover over backgroundArc as does largeArc.
54
Figure 27. About View Showing Depth
The z-order in this example is determined by the order in which the children are added to the Pane.
backgroundArc is obscured by items added later, most notably largeArc. To rearrange the children, use the
toFront() and toBack() methods after the items have been added to the Pane.
vbox.getChildren().add( p );
When starting JavaFX, it is tempting to build an absolute layout. Be aware that absolute layouts are brittle, often
breaking when the screen is resized or when items are added during the software maintenance phase. Yet, there
are good reasons for using absolute positioning. Gaming is one such usage. In a game, you can adjust the (x,y)
coordinate of a 'Shape' to move a game piece around the screen. This article demonstrated the JavaFX class
Pane which provides absolute positioning to any shape-driven UI.
@Override
public void start(Stage primaryStage) throws Exception {
55
Pane p = new Pane();
vbox.getChildren().add( p );
primaryStage.setTitle("Pane App");
primaryStage.setScene( scene );
primaryStage.setWidth( 568 );
primaryStage.setHeight( 320 );
primaryStage.setOnShown( (evt) -> {
hyperlink.setLayoutX( 284 - (hyperlink.getWidth()/3) );
hyperlink.setLayoutY( 160 - hyperlink.getHeight() );
});
primaryStage.show();
}
56
launch(args);
}
}
4.4. Clipping
Most JavaFX layout containers (base class Region [https://docs.oracle.com/javase/8/javafx/api/javafx/scene/layout/
Region.html]) automatically position and size their children, so clipping any child contents that might protrude
beyond the container’s layout bounds is never an issue. The big exception is Pane [https://docs.oracle.com/javase/8/
javafx/api/javafx/scene/layout/Pane.html], a direct subclass of Region and the base class for all layout containers
with publicly accessible children. Unlike its subclasses Pane does not attempt to arrange its children but simply
accepts explicit user positioning and sizing.
Pane does not clip its content by default, so it is possible that children’s bounds may extend outside its own
bounds, either if children are positioned at negative coordinates or the pane is resized smaller than its
preferred size.
This quote is somewhat misleading. Children are rendered (wholly or partially) outside their parent Pane
'whenever' their combination of position and size extends beyond the parent’s bounds, regardless of whether the
position is negative or the Pane is ever resized. Quite simply, Pane only provides a coordinate shift to its
children, based on its upper-left corner – but its layout bounds are completely ignored while rendering children.
Note that the Javadoc for all Pane subclasses (that I checked) includes a similar warning. They don’t clip their
contents either, but as mentioned above this is not usually a problem for them because they automatically arrange
their children.
So to properly use Pane as a drawing surface for Shapes, we need to manually clip its contents. This is
somewhat complex, especially when a visible border is involved. I wrote a small demo application to illustrate
the default behavior and various steps to fix it. You can download it as PaneDemo.zip [http://kynosarges.org/misc/
PaneDemo.zip] which contains a project for NetBeans 8.2 and Java SE 8u112. The following sections explain each
step with screenshots and pertinent code snippets.
57
Figure 28. Child Extending Outside Pane Bounds
As you can see, the Ellipse overwrites its parent’s Border and protrudes well beyond it. The following code
is used to generate the default view. It’s split into several smaller methods, and a constant for the Border corner
radius, because they will be referenced in the next steps.
58
4.4.2. Simple Clipping
Surprisingly, there is no predefined option to have a resizable Region automatically clip its children to its
current size. Instead, you need to use the basic clipProperty [https://docs.oracle.com/javase/8/javafx/api/javafx/scene/
Node.html#clipProperty] defined on Node and keep it updated manually to reflect changing layout bounds. Method
clipChildren below show how this works (with Javadoc because you may want to reuse it in your own
code):
/**
* Clips the children of the specified {@link Region} to its current size.
* This requires attaching a change listener to the region’s layout bounds,
* as JavaFX does not currently provide any built-in way to clip children.
*
* @param region the {@link Region} whose children to clip
* @param arc the {@link Rectangle#arcWidth} and {@link Rectangle#arcHeight}
* of the clipping {@link Rectangle}
* @throws NullPointerException if {@code region} is {@code null}
*/
static void clipChildren(Region region, double arc) {
return pane;
}
Choose Clipped (Alt+C) in PaneDemo to render the corresponding output. Here’s how that looks:
59
Figure 29. Pane with Clip Applied
That’s better. The Ellipse no longer protrudes beyond the Pane – but still overwrites its Border. Also note
that we had to manually specify an estimated corner rounding for the clipping Rectangle in order to reflect
the rounded Border corners. This estimate is 3 * BORDER_RADIUS because the corner radius specified on
Border actually defines its inner radius, and the outer radius (which we need here) will be greater depending on
the Border thickness. (You could compute the outer radius exactly if you really wanted to, but I skipped that
for the demo application.)
Choose Nested (Alt+N) in PaneDemo to render the corresponding output. Now everything looks as it should:
60
Figure 30. Nesting Pane in StackPane
As an added bonus, we no longer need to guesstimate a correct corner radius for the clipping Rectangle. We
now clip to the inner rather than outer circumference of our visible Border, so we can directly reuse its inner
corner radius. Should you specify multiple different corner radii or an otherwise more complex Border you’d
have to define a correspondingly more complex clipping Shape.
There is one small caveat. The top-left corner of the drawing Pane to which all child coordinates are relative
now starts within the visible Border. If you retroactively change a single Pane with visible Border to nested
panes as shown here, all children will exhibit a slight positioning shift corresponding to the Border thickness.
4.5. GridPane
Forms in business applications often use a layout that mimics a database record. For each column in a table, a
header is added on the left-hand side which is matched with a row value on the right-hand side. JavaFX has a
special purpose control called GridPane for this type of layout that keeps contents aligned by row and column.
GridPane also supports spanning for more complex layouts.
This screenshot shows a basic GridPane layout. On the left-hand side of the form, there is a column of field
names: Email, Priority, Problem, Description. On the right-hand side of the form, there is a column of controls
which will display the value of the corresponding field. The field names are of type Label and the value
controls are a mixture including TextField, TextArea, and ComboBox.
61
Figure 31. Field Name / Value Pairs in a GridPane
The following code shows the objects created for the form. "vbox" is the root of the Scene and will also contain
the ButtonBar at the base of the form.
GridPane has a handy method setGridLinesVisible() which shows the grid structure and gutters. It is
especially useful in more complex layouts where spanning is involved because gaps in the row/col assignments
can cause shifts in the layout.
62
Figure 32. Grid Structure and Gutters
4.5.1. Spacing
As a container, GridPane has a padding property that can be set to surround the GridPane contents with
whitespace. "padding" will take an Inset object as a parameter. In this example, 10 pixels of whitespace is
applied to all sides so a short form constructor is used for Inset.
Within the GridPane, vgap and hgap control the gutters. The hgap is set to 4 to keep the fields close to their
values. vgap is slightly larger to help with mouse navigation.
In order to keep the lower part of the form consistent, a Priority is set on the VBox. This will not resize the
individual rows however. For individual resize specifications, use ColumnConstraints and
RowConstraints.
VBox.setVgrow(gp, Priority.ALWAYS );
63
gp.add( lblTitle, 1, 1); // empty item at 0,0
gp.add( lblEmail, 0, 2); gp.add(tfEmail, 1, 2);
gp.add( lblPriority, 0, 3); gp.add( cbPriority, 1, 3);
gp.add( lblProblem, 0, 4); gp.add( tfProblem, 1, 4);
gp.add( lblDescription, 0, 5); gp.add( taDescription, 1, 5);
lblTitle is put in the second column of the first row. There is no entry in the first column of the first row.
Subsequent additions are presented in pairs. Field name Label objects are put in the first column (column
index=0) and value controls are put in the second column (column index=1). The rows are added by the
incremented second value. For example, lblPriority is put in the fourth row along with its ComboBox.
GridPane is an important container in JavaFX business application design. When you have a requirement for
name / value pairs, GridPane will be an easy way to support the strong column orientation of a traditional
form.
@Override
public void start(Stage primaryStage) throws Exception {
VBox.setVgrow(gp, Priority.ALWAYS );
64
gp.add( lblTitle, 1, 1); // empty item at 0,0
gp.add( lblEmail, 0, 2); gp.add(tfEmail, 1, 2);
gp.add( lblPriority, 0, 3); gp.add( cbPriority, 1, 3);
gp.add( lblProblem, 0, 4); gp.add( tfProblem, 1, 4);
gp.add( lblDescription, 0, 5); gp.add( taDescription, 1, 5);
buttonBar.setButtonData(saveButton, ButtonBar.ButtonData.OK_DONE);
buttonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE);
buttonBar.getButtons().addAll(saveButton, cancelButton);
}
65
Figure 33. Spanning Columns
Turing the grid lines on, notice that the former two column grid is replaced with a six column grid. The third row
containing six items — 3 field name / value pairs — dictates the structure. The rest of the form will use
spanning in order to fill in the whitespace.
The VBox and GridPane container objects used in this update follow. There is a little more Vgap to help the
user select the ComboBox controls.
66
GridPane gp = new GridPane();
gp.setPadding( new Insets(10) );
gp.setHgap( 4 );
gp.setVgap( 10 );
VBox.setVgrow(gp, Priority.ALWAYS );
As in the earlier version, the controls are added to the GridPane using the add() method. A column and row
are specified. In this snippet, the indexing it not straightforward as there are gaps expected to be filled in by
spanning content.
67
gp.add( lblTitle, 1, 0); // empty item at 0,0
Finally, the spanning definitions are set using a static method on GridPane. There is a similar method to do
row spanning. Title will take up 5 columns as will Problem and Description. Email shares a row with Contract,
but will take up more columns. The third row of ComboBoxes is a set of three field/value pairs each taking up
one column.
GridPane.setColumnSpan( lblTitle, 5 );
GridPane.setColumnSpan( tfEmail, 3 );
GridPane.setColumnSpan( tfProblem, 5 );
GridPane.setColumnSpan( taDescription, 5 );
Alternatively, a variation of the add() method will columnSpan and rowSpan arguments to avoid the subsequent
static method call.
This expanded GridPane example demonstrated column spanning. The same capability is available for row
spanning which would allow a control to claim additional vertical space. Spanning keeps controls aligned even
in cases where the number of items in a given row (or column) varies. To keep the focus on the spanning topic,
this grid allowed the column widths to vary. The article on ColumnConstraints and RowConstraints
will focus on building true modular and column typographical grids by better controlling the columns (and
rows).
@Override
public void start(Stage primaryStage) throws Exception {
68
gp.setHgap( 4 );
gp.setVgap( 10 );
VBox.setVgrow(gp, Priority.ALWAYS );
69
GridPane.setColumnSpan( lblTitle, 5 );
GridPane.setColumnSpan( tfEmail, 3 );
GridPane.setColumnSpan( tfProblem, 5 );
GridPane.setColumnSpan( taDescription, 5 );
buttonBar.setButtonData(saveButton, ButtonBar.ButtonData.OK_DONE);
buttonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE);
buttonBar.getButtons().addAll(saveButton, cancelButton);
}
This screenshot shows an example modified from previous articles. The demo program for this article has a
rotated feel whereby the field names are paired with the field values vertically (on top of the values) rather than
horizontally. Row spanning and column spanning is used to align items that are larger than a single cell.
70
Figure 35. Example App Using Row and Column Spanning
The red rectangles and text are not part of the UI. They are identifying sections of the screen that will be
addressed later with ColumnConstraints and RowConstaints.
This code is the creation of the Scene root and GridPane objects.
71
VBox vbox = new VBox();
VBox.setVgrow(gp, Priority.ALWAYS );
This code creates the UI controls objects used in the article. Notice that Priority is now implemented as a VBox
containing RadioButtons.
The Label and value control pairings of Email, Contract, Problem, and Description are put in a single column.
They should take the full width of the GridPane so each has its columnSpan set to 2.
72
GridPane.setColumnSpan( tfEmail, 2 );
GridPane.setColumnSpan( tfContract, 2 );
GridPane.setColumnSpan( tfProblem, 2 );
GridPane.setColumnSpan( taDescription, 2 );
The new Priority RadioButtons are matched horizontally with four controls for Severity and Category. This
rowSpan setting instructs JavaFX to put the VBox containing the RadioButton in a merged cell that is four rows
in height.
GridPane.setRowSpan( priorityVBox, 4 );
This code is a RowConstraints object to the GridPane for the TextArea. Prior to the setter,
RowConstraints objects are allocated for all of the other rows. The set method of
getRowConstraints() will throw an index exception when you specify row 12 without first allocating an
object.
73
RowConstraints taDescriptionRowConstraints = new RowConstraints();
taDescriptionRowConstraints.setVgrow(Priority.ALWAYS);
As an alternative syntax, there is a setConstraints() method available from the GridPane. This will pass in
several values and obviates the need for the dedicated columnSpan set call for the TextArea. The
RowConstraints code from the previous listing will not appear in the finished program.
gp.setConstraints(taDescription,
0, 12,
2, 1,
HPos.LEFT, VPos.TOP,
Priority.SOMETIMES, Priority.ALWAYS);
This code identifies the Node at (0,12) which is the TextArea. The TextArea will span 2 columns but only
1 row. The HPos and Vpos are set to the TOP LEFT. Finally, the Priority of the hgrow is SOMETIMES and
the vgrow is ALWAYS. Since the TextArea is the only row with "ALWAYS", it will get the additional space.
If there were other ALWAYS settings, the space would be shared among multiple rows.
74
Figure 37. Wireframe of the Demo App
To make the column widths equal, define two ColumnConstraint objects and use a percentage specifier.
75
Figure 38. App With Extra Space Properly Allocated
GridPane is an important control in developing JavaFX business applications. When working on a requirement
involving name / value pairs and a single record view, use GridPane. While GridPane is easier to use than
the GridBagLayout from Swing, I still find that the API is a little inconvenient (assigning own indexes,
disassociated constraints). Fortunately, there is Scene Builder which simplifies the construction of this form
greatly.
76
public class ConstraintsGridPaneApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
VBox.setVgrow(gp, Priority.ALWAYS );
77
gp.add(tfEmail, 0, 2);
gp.add( lblContract, 0, 3 );
gp.add( tfContract, 0, 4 );
GridPane.setColumnSpan( tfEmail, 2 );
GridPane.setColumnSpan( tfContract, 2 );
GridPane.setColumnSpan( tfProblem, 2 );
GridPane.setRowSpan( priorityVBox, 4 );
gp.setConstraints(taDescription,
0, 12,
2, 1,
HPos.LEFT, VPos.TOP,
Priority.SOMETIMES, Priority.ALWAYS);
buttonBar.setButtonData(saveButton, ButtonBar.ButtonData.OK_DONE);
buttonBar.setButtonData(cancelButton, ButtonBar.ButtonData.CANCEL_CLOSE);
buttonBar.getButtons().addAll(saveButton, cancelButton);
78
Scene scene = new Scene(vbox);
}
4.8. AnchorPane
AnchorPane is a container control that defines its layout in terms of edges. When placed in a container, the
AnchorPane stretches to fill the available space. The children of AnchorPane express their positions and
sizes as distances from the edges: Top, Left, Bottom, Right. If one or two anchor settings are placed on an
AnchorPane child, the child will be fixed to that corner of the window. If more than two anchor settings are
used, the child will be stretched to fill the available horizontal and vertical space.
This mockup shows a TextArea surrounded by a set of controls: a Hyperlink and two status indicators.
Since the TextArea will contain all of the content, it should take up most of the space initially and should
acquire any additional space from a resize. On the periphery, there is a Hyperlink in the upper-right, a
connection Label and Circle in the lower-right, and a status Label in the lower-left.
79
4.8.1. Anchors
To begin the layout, create an AnchorPane object and add it to the Scene.
Anchors are set using static methods of the AnchorPane class. The methods — one per edge — accept the
Node and an offset. For the Hyperlink, an anchor to the top edge and an anchor to the right edge will be set.
An offset of 10.0 is set for each edge so that the link is not compressed against the side.
ap.getChildren().add( signoutLink );
When the screen is resized, the AnchorPane will resize and signoutLink will maintain its top-right position.
Because neither the left nor bottom anchors are specified, signoutLink will not be stretched.
Next, the connection Label and Circle are added. These controls are wrapped in an HBox.
ap.getChildren().add( connHBox );
As with signoutLink, connHBox is fixed to a place on the screen. connHBox is set to be 10 pixels from the
bottom edge and 10 pixels from the right edge.
The lower-left status Label is added. The left and the bottom anchors are set.
80
This is a screenshot of the finished app. The status and control labels are at the bottom of the screen, pinned to
the left and right edges respectively. The Hyperlink is pinned to the top-right.
4.8.2. Resizing
The controls on the periphery may vary in size. For example, a status message or a connection message may be
longer. However, the extra length can be accommodated in this layout by extending the bottom-left status
Label to the right and by extending the bottom-right connection status Label to the left. Resizing with this
layout will move these controls in absolute terms, but they will adhere to their respective edges plus the offset.
That is not the case with the TextArea. Because the TextArea may contain a lot of content, it should receive
any extra space that the user gives the window. This control will be anchored to all four corners of the
AnchorPane. This will cause the TextArea to resize when the window resizes. The TextArea is fixed on
the top-left and as the user drags the window handles to the bottom-right, the bottom-right corner of the
TextArea moves as well.
This picture shows the result of two resize operations. The top screenshot is a vertical resize from dragging the
bottom edge of the window down. The bottom screenshot is a horizontal resize from dragging the right edge of
the window right.
81
Figure 41. "AnchorPane App Resized
The highlighted boxes show that the controls bordering the TextArea retain their positions relative to the
edges. The TextArea itself is resized based on the Window resize. The top and bottom offsets of the
TextArea account for the other controls so that they are not hidden.
ap.getChildren().add( ta );
AnchorPane is a good choice when you have a mixture of resizable and fixed-position children. Other controls
like VBox and HBox with a Priority setting are preferred if there is only one child needing resizing. Use
these controls instead of AnchorPane with a single child that has all four anchors set. Remember that to set an
anchor on a child, you use a static method of the container class such as AnchorPane.setTopAnchor().
82
@Override
public void start(Stage primaryStage) throws Exception {
ap.getChildren().add( signoutLink );
ap.getChildren().add( connHBox );
primaryStage.setTitle("AnchorPaneApp");
primaryStage.setScene( scene );
83
primaryStage.setWidth(568);
primaryStage.setHeight(320);
primaryStage.show();
}
4.9. TilePane
A TilePane is used for grid layout of equally sized cells. The prefColumns and the prefRows properties define
the number of rows and columns in the grid. To add Nodes to TilePane, access the children property and call
the add() or addAll() method. This is easier to use than GridPane which requires explicit setting of the row /
column position of Nodes.
This screenshot shows a TilePane defined as a three-by-three grid. The TilePane contains nine
Rectangle objects.
The complete code for the three-by-three grid follows. The children property of the TilePane provides the
addAll() method to which Rectangle objects are added. The tileAlignment property positions each of the
Rectangle objects in the center of its corresponding tile.
84
ThreeByThreeApp.java
@Override
public void start(Stage primaryStage) throws Exception {
tilePane.getChildren().addAll(
new Rectangle(50, 50, Color.RED),
new Rectangle( 50, 50, Color.GREEN ),
new Rectangle( 50, 50, Color.BLUE ),
new Rectangle( 50, 50, Color.YELLOW ),
new Rectangle( 50, 50, Color.CYAN ),
new Rectangle( 50, 50, Color.PURPLE ),
new Rectangle( 50, 50, Color.BROWN ),
new Rectangle( 50, 50, Color.PINK ),
new Rectangle( 50, 50, Color.ORANGE )
);
primaryStage.setTitle("3x3");
primaryStage.setScene( scene );
primaryStage.show();
}
Since all of the Node contents of the TilePane were equally-sized Rectangles, the layout is packed together
and the tileAlignment setting is not noticeable. When the tilePrefHeight and tilePrefWidth properties are set to be
larger than the contents — say 100x100 tiles containing 50x50 Rectangles — tileAlignment will determine
how the extra space will be used.
See the following modified ThreeByThreeApp class that sets the tilePrefHeight and tilePrefWidth.
tilePane.setPrefTileHeight(100);
tilePane.setPrefTileWidth(100);
85
Figure 43. 3x3 TilePane (Modified)
In the prior screenshots, nine Rectangle objects were provided to the three-by-three grid. If the contents do not
match up with the TilePane definition, those cells will collapse. This modification adds only five Rectangles
rather than nine. The first row contains has content for all three tiles. The second row has content for only the
first two files. The third row is missing entirely.
There is a property "orientation" that instructs TilePane to add items row-by-row (HORIZONTAL, the
default) or column-by-column (VERTICAL). If VERTICAL is used, then the first column will have three
elements, the second column will have only the top two, and the third column will be missing. This screenshot
shows the five Rectangles being added to the three-by-three grid (nine tiles) using VERTICAL orientation.
86
Figure 45. 3x3 App - VERTICAL
4.9.1. Algorithms
It is possible to create JavaFX grid layouts with other containers like GridPane, VBox, and HBox. TilePane is
a convenience that defines the grid layout in advance and makes adding items to the grid a simple add() or
addAll() call. Unlike a grid layout built with a combination of nested VBox and HBox containers, the
TilePane contents are direct children. This makes it easy to loop over the children during event processing
which helps implement certain algorithms.
This example app places four Circles in a TilePane. An event handler is attached to the TilePane which
looks for a selection of one of the Circles. If a Circle is selected, it is dimmed through the opacity setting. If the
Circle is re-selected, its original color is restored. This screenshot shows the app with the blue Circle
appearing purple-ish because it has been selected.
The program begins by adding the items and setting a custom property "selected" using the Java 8 Stream API.
87
TileApp.java
circles
.stream()
.forEach( (c) -> c.getProperties().put( "selected", Boolean.FALSE
));
tilePane.getChildren().addAll(
circles
);
Next, the event handler is attached to the mouse event. This is also using Java 8 Streams. The filter() method is
determining whether or not a Circle is selected using the Node.contains() method on converted coordinates. If
that expression passes, findFirst() is used to retrieve the first (and in this case, only) match. The block of code in
ifPresent() sets the "selected" flag for keeping track of the Circle state and tweaks the opacity.
88
TileApp.java
tilePane.setOnMouseClicked(
TileApp.java
scene.setOnKeyPressed(
(evt) -> {
if( evt.getCode().equals(KeyCode.S) ) {
Collections.shuffle( circles );
tilePane.getChildren().clear();
tilePane.getChildren().addAll( circles );
}
}
);
While feasible, a grid built with VBoxes and HBoxes would be slightly more difficult because of the nested
structures. Also, TilePane will not stretch the contents to fill extra space, making it suitable for composite
89
controls that need to be packed together for ergonomic reasons.
TilePane creates a grid based layout of equally sized cells. Contents are added to the TilePane based on the
prefRows, prefColumns, and orientation settings. If the grid contains more tiles than added Nodes, there will be
gaps in the layout and rows and columns may collapse if no content was provided whatsoever. This post showed
a pair of algorithms that were implemented easily because of TilePane’s simply interface.
TileApp.java (Complete)
@Override
public void start(Stage primaryStage) throws Exception {
circles
.stream()
.forEach( (c) -> c.getProperties().put( "selected", Boolean.FALSE
));
tilePane.getChildren().addAll(
circles
);
tilePane.setOnMouseClicked(
90
)
)
.findFirst()
.ifPresent(
(c) -> {
Boolean selected = (Boolean)
c.getProperties().get("selected");
if( selected == null || selected ==
Boolean.FALSE ) {
c.setOpacity(0.3d);
c.getProperties().put("selected",
Boolean.TRUE);
} else {
c.setOpacity( 1.0d );
c.getProperties().put("selected",
Boolean.FALSE);
}
}
)
);
scene.setOnKeyPressed(
(evt) -> {
if( evt.getCode().equals(KeyCode.S) ) {
Collections.shuffle( circles );
tilePane.getChildren().clear();
tilePane.getChildren().addAll( circles );
}
}
);
primaryStage.setTitle("TileApp");
primaryStage.setScene( scene );
primaryStage.show();
}
4.10. TitledPane
A TitledPane is a Node container matched with a Label and an optional control for showing and hiding the
container contents. Since TitledPane is limited to a single Node, it is often paired with a container
supporting multiple children like a VBox. Functionally, it can hide non-essential details of a form or group
related controls.
91
This example is a web search app that accepts a set of keywords in a TextField. The user presses the Search
Button to run a search. The Advanced TitlePane expands to provide additional search arguments.
This screenshot shows the un-expanded state which is the view for a user executing a simple keyword search.
This next screenshot shows the view for a user requiring advanced search parameters. The Advanced TitledPane
was expanded by pressing on the arrow in the TitledPane header.
To create a TitledPane, use the constructor to pass in a String title and a single Node child. The default
constructor can also be used and the title and Node set using setters. This code uses the parameterized
92
constructor. A VBox is the single child of the TitledPane. However, the VBox itself contains several
controls.
TitledPaneApp.java
By default, the TitledPane will be expanded. This does not fit the use case of hiding non-essential
information, so the expanded property is set after the object is created.
4.10.1. Collapsible
Another property of TitledPane is collapsible. By default, the TitledPane collapsible property is set to
true. However, a quick grouping can be provided to controls that are not collapsible. The following screenshot
demonstrates this use case.
93
This code sets the collapsible flag after the constructor is called.
94
public class TitledPaneApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
vbox.getChildren().addAll(
titledPane,
new Button("Search")
);
primaryStage.setTitle( "TitledPaneApp" );
primaryStage.setScene( scene );
primaryStage.setWidth( 568 );
primaryStage.setHeight( 320 );
primaryStage.show();
}
95
96
Chapter 5. CSS
Placeholder whilst things get built…
97
98
Chapter 6. Performance
Placeholder whilst things get built…
99
100
Chapter 7. Application Structure
7.1. The MVVM Pattern
Model-View-ViewModel (MVVM) is a software architecture that separates UI controls (the View) from data
access and business logic (the Model). The separation helps larger applications during the maintenance phase.
With MVVM, View changes — especially those that are purely cosmetic — can be made without fear of
introducing side effects. Changes to the Model (usually less volatile than the View) can be applied more easily
throughout the application because MVVM promotes reuse.
In between the View and the Model is the ViewModel. The ViewModel mediates between the View and the
Model, yet contains no references to the View. This enables ViewModel sharing which is useful when two Views
show similar data.
This article presents a simplified MVVM example. An Employment Request Form submits several data elements
to a back-end service. This screenshot shows the application with the name, position, and annual salary data
elements set.
After entering the data and pressing Save, the Model object responds with a println().
Program Output
If the Reset button is pressed after filling in the TextFields, the form is restored to its original values: empty
name and position and an annualSalary of 0.
7.1.1. Structure
A simplified MVVM application is composed of the following types of classes
101
App
Main entry point
View
UI controls
Model
Function call to business logic and data access
ViewModel
Contains screen state and UI logic
Domain object
UI-neutral transfer object
Converter
Helper class for ViewModel to Model communication
This UML shows the structure of the Employment Request Form. The View class is EmploymentRequestView
which contains the UI control objects like the Name TextField. The ViewModel class is
EmploymentRequestViewModel and contains JavaFX properties. The Model class is EmploymentRequestModel
with a single method for persisting the form. EmploymentRequestConverter is a helper class.
EmploymentRequest is a POJO containing data fields. MVVMApp is the main entry point, a JavaFX
Application subclass.
7.1.2. View
In MVVM, the View class is responsible for the UI controls and nothing else. Event handlers attached to UI
controls delegate immediately to the ViewModel. JavaFX data binding updates the UI with Model changes. In
the Employment Request Form, three TextFields gather input from the user: tfName, tfPosition, tfAnnualSalary.
Three Buttons initiate operations on the application: btnSave, btnCancel, btnReset. This is the beginning of the
EmploymentRequestView class.
102
EmploymentRequestView.class
public EmploymentRequestView() {
createView();
bindViewModel();
}
The View knows about the ViewModel and uses JavaFX binding to synchronize the UI with the ViewModel.
This demo treats the ViewModel as a prototype object, created when the View object is created. Alternatively,
the ViewModel can be a singleton or provided by CDI. Each UI field is bound bi-directionally to the
ViewModel. As the UI changes — say through entering a value in tfName — the corresponding field in the
ViewModel is updated. The more complicated expression for the tfAnnualSalary field is needed to convert the
String-based TextField into a DoubleProperty.
EmploymentRequestView.class (cont.)
tfName.textProperty().bindBidirectional(viewModel.nameProperty());
tfPosition.textProperty().bindBidirectional(viewModel.positionProperty());
Bindings.bindBidirectional(
tfAnnualSalary.textProperty(),
viewModel.annualSalaryProperty(),
new NumberStringConverter()
);
}
The UI in this demo is built in code. The following createView() method handles the layout of the form and puts
the core controls (such as tfName and btnSave) in containers.
103
EmploymentRequestView.class (cont.)
gpwrap.getChildren().add( gp );
btnSave.setOnAction( this::save );
btnCancel.setOnAction( this::cancel );
btnReset.setOnAction( this::reset );
btnSave.setDefaultButton(true);
this.getChildren().addAll(
gpwrap,
new Separator(),
buttonBar);
}
The class ends with handlers for the Buttons. These handlers delegate their actions to the ViewModel.
104
EmploymentRequestView.class (cont.)
In this example, the UI is built in code. Scene Builder is a design-oriented alternative that can be more
productive. To convert this example to FXML, the product of Scene Builder, you would build the UI in the tool
and annotate the fields and methods of the EmploymentRequestView class with @FXML. See the following
screenshot for how the demo looks in Scene Builder. This is informational only and not part of the working
demo.
Notice that the right "half" of the UML diagram would not be changed by switching the View implementation
from code to Scene Builder. A sign of a good design is when alternative presentations can be supported easily.
7.1.3. ViewModel
The ViewModel is a layer that interacts with both the View and the Model. In this simple presentation, the value
of such a layer is limited; one could just unpack the TextFields into the Model in the Button handler. As the UI
becomes more complex, it is useful to have a plainer, more business-oriented object to work with. While there is
a one-to-one correspondence between View, Model, and ViewModel, that may not always be the case.
105
Handling a many-to-many relationship is key for the ViewModel. There may be many Views that working with
the same Model element. Multiple models may contribute to a single View.
This ViewModel looks like the domain object that will be presented later with one key difference: JavaFX
Binding. EmploymentRequestViewModel was bound to the EmploymentRequestView UI controls and the
methods of EmploymentRequestViewModel will have access to all of the information within the save() method.
No extra marshaling of arguments is needed.
EmploymentRequestViewModel.class
106
public void setAnnualSalary(double annualSalary) {
this.annualSalary.set(annualSalary);
}
Both the Converter and the Model have been added to this ViewModel as prototypes, meaning that they were
created when the ViewModel was created.
Converter
The Converter is a class the translates between ViewModel and domain object. In this app, there is a single
toEmploymentRequest() method that creates an object from the ViewModel fields.
EmploymentRequestConverter.class
7.1.4. Model
Finally, the Model persists the information. This Model example has a single mocked method which will verify
that it receives the correct data for the save() operation.
107
EmploymentRequestModel.class
This is the plain Java object used to transport data from the Model to the UI.
EmploymentRequest.class
@Override
public String toString() {
return "EmploymentRequest{" +
"name='" + name + '\'' +
", position='" + position + '\'' +
", annualSalary=" + annualSalary +
'}';
}
}
JavaFX provides developers with a powerful toolkit to build applications. However, a design is still needed for
an effective program. MVVM is an architectural pattern that separates pure UI classes called Views from pure
108
data classes called Models. In the middle sits the ViewModel which relies heavily on the data binding in JavaFX.
To read more about MVVM and JavaFX, check out the mvvmFX project. The examples in that project provided
the basis for this demonstration.
mvvmFX [https://github.com/sialcasa/mvvmFX]
bkcourse_mvvmapp_sources.zip [https://courses.bekwam.net/public_tutorials/source/bkcourse_mvvmapp_sources.zip]
1. Asynchrony,
2. A ProgressBar and status Label, and
3. Event broadcasting.
All operations taking more than a few milliseconds should be run on a separate Thread. Something that runs
quickly but involves IO or the network often turns into a performance problem as code moves to new computers
and new network configurations. A JavaFX Task is used to invoke the Model operation. While this potentially
long process is running, feedback is given to the user via a ProgressBar and Label. The status Label
conveys messages to the user from the JavaFX Task.
It is important that the Model not hold a reference to the ViewModel, so an event notification scheme is
introduced. The ViewModel listens for an EVENT_MODEL_UPDATE message from the Model. Although this
example uses only one ViewModel, this scheme makes it possible for more than one ViewModel to be aware of
data changes from a single Model component.
109
Figure 53. A Successful Run
The UI remains responsive throughout the request. The responsiveness comes from the use of a JavaFX Task to
run the URL retrieval on a background thread. To make the user aware that processing is occurring, UI controls
are tied to the properties of the Task through JavaFX binding. This screenshot shows the feedback the user
receives while the Task runs.
When the Submit Button is pressed, a pair of controls are displayed: a ProgressBar and a Label. Both
controls are updated with information about the running background Thread.
Errors in the URL retrieval are handled by passing an alternate response object out of the Model. In the
successful case, the Model returned the HTTP status code and elapsed time. In the error case, the Model sets a
flag and returns an error message. This screenshot shows an error dialog produced by the View in response to an
error in the Model. The errorMessage is from the message property of the thrown Exception. If needed,
additional fields like the Exception class name can be added.
110
Figure 55. Error Produced by Model
7.2.2. Design
The demo program consists of a single View / ViewModel / Model triple. The View communicates with the
ViewModel through JavaFX binding. UI controls in the View are bound to JavaFX properties in the ViewModel.
Event handlers in the View delegate to methods in the ViewModel. The ViewModel forms an asynchronous
command which interacts with the Model. The Model communicates indirectly with the ViewModel through a
notification subsystem rather than an object reference.
URLTestView is the View component and contains the UI controls. The ViewModel contains properties for the
domain — url, last status code, last elapsed time — and for the screen state such as urlTestTaskRunning.
Model contains a service call and works with a UI-neutral POJO URLTestObject. Communication between the
Model and the ViewModel is brokered through a Notifications singleton which has methods for publishing (for
the Model) and subscribing (for the ViewModel).
111
This sequence diagram shows how the app wires itself up and the interactions that follow from a test operation.
After all the objects are created, the user initiates a test operation. This results in a TestURLCommand object
being created which is a JavaFX Service. The service invokes a Model method testURL(). When testURL()
finishes, it publishes a notification. This notification triggers a call to the ViewModel to refresh itself which uses
a second call to the Model. The ViewModel refresh sets ViewModel JavaFX properties which automatically
update the View.
7.2.3. View
The View is a StackPane containing the TextField that will gather the URL input and a Submit Button. A
StackPane was used so that the temporary status display could be added without breaking the centering of the
main UI controls. The HBox containing the status Label and ProgressBar is always present in the lower-
left, but hidden unless a Task is running.
112
URLTestView.class
public URLTestView() {
StackPane.setAlignment(statusHBox, Pos.BOTTOM_LEFT );
The URLTestViewModel object is created in this class. Alternatively, dependency injection can be used to
distribute the same ViewModel object among other Views.
The URLTestView constructor continues with several binding expressions. These link the UI controls to the
ViewModel properties.
URLTestView.class (cont.)
lblStatus.textProperty().bind( testViewModel.statusCodeProperty() );
lblLoadTime.textProperty().bind( testViewModel.loadTimeProperty() );
testViewModel.urlProperty().bind( tfURL.textProperty() );
statusHBox.visibleProperty().bind(testViewModel.urlTestTaskRunningProperty() );
pb.progressProperty().bind( testViewModel.urlTestTaskProgressProperty()
);
lblTaskStatus.textProperty().bind(
testViewModel.urlTestTaskMessageProperty());
113
The above statements register the UI controls for changes to the corresponding property in the ViewModel,
except for tfURL. tfURL uses a different binding direction since it is producing the value for the ViewModel. In
some cases, the binding may need to be bi-directional if a control can both be manipulated by the user and set
from the ViewModel.
The action which initiates the testURL() operation is mapped to the Submit Button.
URLTestView.class (cont.)
The URLTestView constructor finishes with a special ChangeListener binding to a ViewModel property. This is a
notification that an error has occurred. When the errorMessage property of the ViewModel is notified, the View
displays a popup dialog.
URLTestView.class (cont.)
testViewModel.errorMessageProperty().addListener(
(obs,ov,nv) -> {
if( nv != null && !nv.isEmpty() ) {
Alert alert = new Alert(
Alert.AlertType.ERROR, nv
);
alert.showAndWait();
}
}
);
7.2.4. ViewModel
URLTestView binds its UI controls to properties in URLTestViewModel. This section of the class
URLTestViewModel shows the properties used by the View and their corresponding access methods. The test()
method — which was mapped to the Submit Button press event — is also listed. The object
urlTestCommand will be presented later.
114
URLTestViewModel.class
// Data elements
private final StringProperty url = new SimpleStringProperty("");
private final StringProperty statusCode = new SimpleStringProperty("");
private final StringProperty loadTime = new SimpleStringProperty("");
// Status elements
private final BooleanProperty wasError = new SimpleBooleanProperty(false);
private final StringProperty errorMessage = new SimpleStringProperty("");
115
URLTestViewModel.class (cont.)
public URLTestViewModel() {
notifications.subscribe(Notifications.EVENT_MODEL_UPDATE,
this,
this::update); // presented later
}
Command
URLTestViewModel.class (cont.)
A JavaFX Service was used since the Service objects needs to always exist for binding purposes.
URLTestView binds its ProgressBar, status Label, and container controls to the URLTestViewModel object
which will be available for the life of the app. Shown earlier, the URLTestViewModel properties delegate to the
Service object. A Task is a one-shot invocation and using that would not work for multiple test()
invocations.
Asynchrony
The design in this article puts the burden of asynchronous processing on the ViewModel. This provides
direct feedback to View controls using JavaFX binding. An alternative approach is to use a general event
emitting scheme to listen for task starting, task ending, progress, and message events. This would support
breaking out the urlTestCommand Service subclass into a separate code module.
116
The presentation of the URLTestViewModel class concludes with the update() method. This method issues a call
to the Model, unpacks the results, and updates the ViewModel properties. Recall that the View has bound to
these properties and will automatically be updated (there is no similar update() method in the View.
URLTestViewModel.class (cont.)
urlTestModel.getUrlTestObject().ifPresent(
(testObject) -> {
wasError.set( testObject.getWasError() );
if( !testObject.getWasError() ) {
statusCode.set(
"Status code: " +
String.valueOf(testObject.getStatusCode())
);
loadTime.set(
String.valueOf(testObject.getLoadTime()) +
" ms"
);
errorMessage.set(testObject.getErrorMessage());
} else {
statusCode.set(""); // use empty TextField, not 0
loadTime.set(""); // use empty TextField, not 0
errorMessage.set( testObject.getErrorMessage() );
}
});
}
7.2.5. Model
URLTestModel is presented in its entirety below. URLTestModel maintains a copy of a domain object. Upon
initialization, this object is empty so an Optional is used. A getter is provided for ViewModels. The testURL()
method issues an HTTP GET call and records the results in the URLTestObject member. If the HTTP GET call is
successful, the URLTestObject will contain the status code (probably 200) and an elapsed time. If unsuccessful,
the URLTestObject will set a convenient wasError flag and an errorMessage.
When the Model has retrieved the contents at the URL or generated an error, the publish() method of the
Notifications object is invoked. This prompts URLTestViewModel to update itself, but in a decoupled fashion. It
is important to note that URLTestModel does not hold a reference to a URLTestViewModel object.
117
URLTestModel.class
try {
long startTimeMillis = System.currentTimeMillis();
HttpURLConnection urlConnection =
(HttpURLConnection) new URL(url).openConnection();
try (
InputStream is = urlConnection.getInputStream();
) {
while (is.read() != -1) {
}
}
long endTimeMillis = System.currentTimeMillis();
urlTestObject = Optional.of(uto);
} catch(Exception exc) {
URLTestObject uto = new URLTestObject(exc.getMessage());
urlTestObject = Optional.of(uto);
}
notifications.publish(Notifications.EVENT_MODEL_UPDATE);
return urlTestObject;
}
}
URLTestModel also does not attempt to bind to URLTestViewModel using JavaFX. Since the asynchrony is
handled at the ViewModel layer, the Model is free to operate off of the JavaFX Thread. Attempting to double-
bind (View→ViewModel→Model) would result in an application threading error if binding were used. Wrapped
in a Platform.runLater(), a double-bind does not violate the prescribed dependency order — ViewModel
already holds a reference to Model — but might result in an inconsistent update.
118
This POJO is the domain object used by the Model. As a POJO, this is can be maintained in a commons library
and shared among non-UI components like a RESTful web services project.
URLTestObject.class
7.2.6. Notifications
This class is a lightweight pub/sub implementation. Event types are registered as String constants at the top of
the file. Subscribers are identified by their class hashCode. All the published events will run on the JavaFX
Thread.
Notifications.class
119
public class Notifications {
Platform.runLater( () -> {
List<SubscriberObject> subscriberList =
instance.subscribers.get(event);
if (subscriberList != null) {
subscriberList.forEach(
subscriberObject -> subscriberObject.getCb().accept(event)
);
if( !instance.subscribers.containsKey(event) ) {
List<SubscriberObject> slist = new ArrayList<>();
instance.subscribers.put( event, slist );
}
if (subscriberList == null) {
subscriberList.remove( subscriber );
}
}
120
private final Object subscriber;
private final Consumer<String> cb;
@Override
public int hashCode() {
return subscriber.hashCode();
}
@Override
public boolean equals(Object obj) {
return subscriber.equals(obj);
}
}
}
7.2.7. App
For completeness, the Application subclass is listed below.
121
ModelChangeApp.class
@Override
public void start(Stage primaryStage) throws Exception {
MVVM is an architecture that separates the View from the Model. Unlike other architectures, this separation
includes a specific dependency graph: View depends on ViewModel depends on Model. All three component
types collaborate, but in cases where data moves in the opposite direction of the dependency graph, the
communication is indirect. In this example, the indirect communication was provided by JavaFX binding and a
special Notifications class. By keeping the Model and ViewModel free of View dependencies, the MVVM
architecture fosters reuse. URLTestModel can be used by other ViewModels and URLTestViewModel can be
used by other Views.
bkcourse_mvvmapp_sources.zip [https://courses.bekwam.net/public_tutorials/source/bkcourse_mvvmapp_sources.zip]
This section will demonstrate the Dialog class built on a domain object, ConnectionInfo. A main screen is
displayed with a TextField for a database URL. Pressing the set Button displays the Dialog. If the user fills
in values and presses the Save Button, the Dialog is dismissed and the ConnectionInfo domain object is returned
to the caller. If the Cancel Button is pressed, an empty Optional is returned.
This screenshot shows the app when it starts up. The DB URL field is empty.
122
Figure 58. DialogApp At Startup
Pressing the Set Button displays the Dialog. The user has filled in values for host, username, and password.
Closing the Dialog via the Save Button forms a ConnectionInfo object that is returned to the caller. This value is
formed into a String and put into the TextField.
123
Figure 60. Values Retrieved
The reverse interaction is also supported in this example. If the user types in a well-formed URL, that URL will
be parsed and displayed in the Dialog. URL String validation has been left off. An invalid URL String will result
in an empty Dialog.
7.3.1. App
The JavaFX Application subclass adds UI controls for the DB URL TextField and Save Button.
DialogApp.java
@Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setTitle("Dialog App");
primaryStage.setScene( scene );
primaryStage.show();
}
showSetDialog() is a method reference that initializes a ConnectionInfo object, displays the Dialog, and retrieves
124
a value if set by the user.
DialogApp.java
Optional<ConnectionInfo> ci = dialog.showAndWait();
The app is using a JavaFX StringConverter to encapsulate the code behind forming a String from the set of
fields of the ConnectionInfo object. The StringConverter is stored as a field in the Application subclass.
125
DialogApp.java
@Override
public String toString(ConnectionInfo c) {
return String.format( format, c.getUsername(), c.getPassword(),
c.getHost() );
}
@Override
public ConnectionInfo fromString(String s) {
return null;
}
}
7.3.2. Dialog
The Dialog subclass adds UI controls to the DialogPane field in the constructor. Notice the lack of explicit
ActionEvent handlers. When using Dialog or Alert, ButtonType and ButtonData are preferred over raw
Button objects. These higher-order objects make the app UI more consistent because the Button placement,
labeling, and behavior is handed in the Dialog abstraction.
126
ConnectionDialog.java
vbox.setSpacing( 10.0d );
vbox.setPadding( new Insets(40.0d) );
DialogPane dp = getDialogPane();
init( initialData );
}
The init() method sets the Dialog UI controls based on the ConnectionInfo fields.
ConnectionDialog.java
The setResultConverter() is the mechanism by which the Dialog will communicate its domain object back to the
caller. The converter is a callback that returns a ConnectionInfo object if one can be formed from the input. In
this case, the Dialog makes a decision to send back an object if the Save Button was pressed. Validating the
Dialog fields can be performed as part of the TextField themselves or as an EventFilter attached to the Save
127
Button.
ConnectionDialog.java
ConnectionInfo.java
The JavaFX Dialog and Alert subclass are windows that provide a simpler interface and a more consistent style
than a raw Stage. Alert is the preferred class when a warning, confirmation, or single value needs to be retrieved
from the user. Dialog is used for complex, but contained, interactions with the user. This example showed how a
main Stage can keep its view simple by delegating the retrieval of detailed information to a Dialog.
128
Dialog — when paired with a type parameter — improves information hiding in an app by turning a
showAndWait() call into a function that returns a value.
7.3.4. Source
The complete source code and Gradle project can be found at the link below.
129
130
Chapter 8. Best Practices
8.1. Styleable Properties
A JavaFX property can be styled via css by using a StyleableProperty. This is useful when controls need
properties that can be set via css.
In order to use a StyleableProperty on a Control, one needs to create a new CssMetaData using the
StyleableProperty. CssMetaData created for a control needs to be added to the List<CssMetaData> obtained
from the control’s ancestor. This new list is then returned from the getControlCssMetaData().
By convention, control classes that have CssMetaData will implement a static method getClassCssMetaData()
and it is customary to have getControlCssMetaData() simply return getClassCssMetaData(). The purpose of
getClassCssMetaData() is to allow sub-classes to easily include the CssMetaData of some ancestor.
131
// StyleableProperty
private final StyleableProperty<Color> color =
new SimpleStyleableObjectProperty<>(COLOR, this, "color");
// CssMetaData
private static final CssMetaData<MY_CTRL, Paint> COLOR =
new CssMetaData<MY_CTRL, Paint>("-color", PaintConverter.getInstance(),
Color.RED) {
@Override
public boolean isSettable(MY_CTRL node) {
return node.color == null || !node.color.isBound();
}
@Override
public StyleableProperty<Paint> getStyleableProperty(MY_CTRL node) {
return node.color;
}
};
@Override
public List<CssMetaData<? extends Styleable, ?>> getControlCssMetaData() {
return getClassCssMetaData();
}
132
Creation of StyleableProperty and CssMetaData needs a lot of boiler-plate code and this can be reduced by using
StyleablePropertyFactory [https://docs.oracle.com/javase/8/javafx/api/javafx/css/StyleablePropertyFactory.html].
StyleablePropertyFactory contains methods to create StyleableProperty with corresponding CssMetaData.
// StyleableProperty
private final StyleableProperty<Color> color =
new SimpleStyleableObjectProperty<>(COLOR, this, "color");
// StyleablePropertyFactory
private static final StyleablePropertyFactory<MY_CTRL> FACTORY =
new StyleablePropertyFactory<>(Control.getClassCssMetaData());
8.2. Tasks
This article demonstrates how to use a JavaFX Task to keep the UI responsive. It is imperative that any operation
taking more than a few hundred milliseconds be executed on a separate Thread to avoid locking up the UI. A
Task wraps up the sequence of steps in a long-running operation and provides callbacks for the possible
outcomes.
The Task class also keeps the user aware of the operation through properties which can be bound to UI controls
like ProgressBars and Labels. The binding dynamically updates the UI. These properties include
133
8.2.1. Demonstration
The following screenshots show the operation of an HTML retrieval application.
Entering a URL and pressing "Go" will start a JavaFX Task. When running, the Task will make an HBox visible
that contains a ProgressBar and a Label. The ProgressBar and Label are updated throughout the operation.
When the retrieval is finished, a succeeded() callback is invoked and the UI is updated. Note that the succeeded()
callback takes place on the FX Thread, so it is safe to manipulate controls.
134
Figure 62. Screenshot of App Showing Successful Retrieval
If there was an error retrieving the HTML, a failed() callback is invoked and an error Alert is shown. failed() also
takes place on the FX Thread. This screenshot shows invalid input. An "h" is used in the URL instead of the
correct "http".
135
8.2.2. Code
An event handler is placed on the Get HTML Button which creates the Task. The entry point of the Task is the
call() method which starts by calling updateMessage() and updateProgress(). These methods are executed on the
FX Thread and will result in updates to any bound properties.
The program proceeds by issuing an HTTP GET using standard java.net classes. A String "retval" is built up
from the retrieved characters. The message and progress properties are updated with more calls to
updateMessage() and updateProgress(). The call() method ends with a return of the String containing the HTML
text.
On a successful operation, the succeeded() callback is invoked. getValue() is a Task method that will return the
value accrued in the Task (recall "retval"). The type of the value is what is provided in the generic argument, in
this case "String". This could be a complex type like a domain object or a Collection. The succeeded() operation
runs on the FX Thread, so the getValue() String is directly set on the TextArea.
If the operation failed, an Exception is thrown. The Exception is caught by the Task and converted to a failed()
call. failed() is also FX Thread-safe and it displays an Alert.
@Override
protected String call() throws Exception {
HttpURLConnection c = null;
InputStream is = null;
String retval = "";
try {
} finally {
if( is != null ) {
is.close();
}
if( c != null ) {
c.disconnect();
}
}
136
updateMessage("HTML retrieved");
updateProgress( 1.0d, 1.0d );
return retval;
}
@Override
protected void succeeded() {
contents.setText( getValue() );
}
@Override
protected void failed() {
Alert alert = new Alert(Alert.AlertType.ERROR,
getException().getMessage() );
alert.showAndWait();
}
};
Notice that the Task does not update the ProgressBar and status Label directly. Instead, the Task makes safe calls
to updateMessage() and updateProgress(). To update the UI, JavaFX binding is used in the following statements.
bottomControls.visibleProperty().bind( task.runningProperty() );
pb.progressProperty().bind( task.progressProperty() );
messageLabel.textProperty().bind( task.messageProperty() );
To run the Task, create a Thread providing the Task as a constructor argument and invoke start().
new Thread(task).start();
For any long-running operation — File IO, the Network — use a JavaFX Task to keep your application
responsive. The JavaFX Task gives your application a consistent way of handling asynchronous operations and
exposes several properties that can be used to eliminate boilerplate and programming logic.
137
private TextArea contents;
@Override
public void start(Stage primaryStage) throws Exception {
Parent p = createMainView();
primaryStage.setTitle("ProgressBarApp");
primaryStage.setWidth( 667 );
primaryStage.setHeight( 376 );
primaryStage.setScene( scene );
primaryStage.show();
}
pb = new ProgressBar();
messageLabel = new Label("");
bottomControls.getChildren().addAll(pb, messageLabel);
return vbox;
}
138
public void getHTML(ActionEvent evt) {
@Override
protected String call() throws Exception {
HttpURLConnection c = null;
InputStream is = null;
String retval = "";
try {
} finally {
if( is != null ) {
is.close();
}
if( c != null ) {
c.disconnect();
}
}
updateMessage("HTML retrieved");
updateProgress( 1.0d, 1.0d );
return retval;
}
@Override
protected void succeeded() {
contents.setText( getValue() );
}
@Override
protected void failed() {
Alert alert = new Alert(Alert.AlertType.ERROR,
getException().getMessage() );
139
alert.showAndWait();
}
};
bottomControls.visibleProperty().bind( task.runningProperty() );
pb.progressProperty().bind( task.progressProperty() );
messageLabel.textProperty().bind( task.messageProperty() );
new Thread(task).start();
}
The null value is a problem when the ComboBox drives other logic like an upper-case transformation or the
lookup of a database record. While a null check is usually used to prevent this type of error, an empty object is
preferred in order to simplify the code. ComboBoxes often appear in clusters and the empty object technique
reduces null checks in the interaction of related ComboBoxes and on save and load operations.
This article presents a pair of related ComboBoxes. A country selection in one ComboBox modifies the list of
available city items in a second ComboBox. Neither selection is required. The user can press the Save Button
at any time and if no selection is made for either ComboBox, an empty object — in this case an empty
String — will be returned.
This is a screenshot of the app. Selecting "Switzerland" from an empty initial value will fill the city ComboBox
with Swiss cities. Selecting the city "Zurich" and pressing Save will retrieve those values.
140
Figure 64. Related ComboBoxes
141
NoNullComboApp.class
countries.add(COUNTRY_FR); countries.add(COUNTRY_DE);
countries.add(COUNTRY_CH);
citiesMap.put(COUNTRY_FR, frenchCities );
citiesMap.put(COUNTRY_DE, germanCities );
citiesMap.put(COUNTRY_CH, swissCities );
}
To retrieve the set of cities for a given country, use the get() method of the Map. The containsKey() method can
be used to determine whether or not the Map contains a value for the specified country. In this example,
containsKey() will be used to handle the empty object case.
8.3.2. UI
The UI is a pair of ComboBoxes with Labels and a Save Button. The controls are put in a VBox and left-
justified. The VBox is wrapped in a TilePane and centered. The TilePane was used since it does not stretch
the VBox horizontally.
142
NoNullComboApp.class
@Override
public void start(Stage primaryStage) throws Exception {
initData();
NoNullComboApp.class
country.getItems().add("");
country.getItems().addAll( countries );
country.setValue( "" ); // empty selection is object and not null
city.getItems().add("");
city.setValue( "" );
In this app, the Country ComboBox will not be changed, so its items are added in the start() method. Country
starts with an initial empty selection as does city. City — at this point — contains a single empty item.
8.3.4. Interaction
When the country value is changed, the contents of the city ComboBox should be replaced. It is common to use
clear() on the backing list; however, this will produce a null value in the ComboBox (no items, no value).
143
Instead, use removeIf() with a clause to keep a single empty item. With the list cleared of all data (except the
empty item), the newly-selected contents can be added with addAll().
NoNullComboApp.class
The Save Button action will print out the values. In no case will a null value be returned from getValue().
If you’re a Java developer, you’ve written "if not null" thousands of times. Yet, project after project, I see
NullPointerExceptions highlighting cases that were missed or new conditions that have arisen. This article
presented a technique for keeping empty objects in ComboBoxes by setting an initial value and using removeIf()
rather than clear() when changing lists. Although, this example used String objects, this can be expanded to work
with domain objects that have an hashCode/equals implementation, an empty object representation, and a
cellFactory or toString() to produce an empty view.
NoNullComboApp.class
@Override
public void start(Stage primaryStage) throws Exception {
144
VBox vbox = new VBox(
countryLabel,
country,
cityLabel,
city,
saveButton
);
vbox.setAlignment(Pos.CENTER_LEFT );
vbox.setSpacing( 10.0d );
initData();
country.getItems().add("");
country.getItems().addAll( countries );
country.setValue( "" ); // empty selection is object and not null
city.getItems().add("");
city.setValue( "" );
primaryStage.setTitle("NoNullComboApp");
primaryStage.setScene( scene );
primaryStage.setWidth( 320 );
primaryStage.setHeight( 480 );
primaryStage.show();
}
145
private void initData() {
countries.add(COUNTRY_FR); countries.add(COUNTRY_DE);
countries.add(COUNTRY_CH);
citiesMap.put(COUNTRY_FR, frenchCities );
citiesMap.put(COUNTRY_DE, germanCities );
citiesMap.put(COUNTRY_CH, swissCities );
}
}
8.4.1. Background
Documenting JavaFX APIs may not appear very different from documenting a Java API but most of us are
unaware of the tools present to ease our work.
While writing a JavaFX application or designing a JavaFX control, a developer adds various JavaFX properties
which normally consists of a field and three methods i.e. JavaFX property method, setter and getter. These
methods are generally public and therefore, should be documented. Writing Javadoc for all the three methods
doesn’t makes sense when most of it is implied. Nevertheless, there should be some documentation and a way to
show a link between all these methods.
The JavaFX team has been very thoughtful and introduced a special option "-javafx" for the javadoc command.
This option introduces the following flexibility:
• Generates HTML documentation using the JavaFX extensions to the standard doclet. The generated
documentation includes a "Property Summary" section in addition to the other summary sections
generated by the standard Java doclet. The listed properties are linked to the sections for the getter and
setter methods of each property.
• If there are no documentation comments written explicitly for getter and setter methods, the
documentation comments from the property method are automatically copied to the generated
documentation for these methods.
146
• Adds a new @defaultValue tag that allows documenting the default value for a property.
• Adds a new @treatAsPrivate tag that adds the flexibility to not publish the doc for a public method
which is a part of the implementation detail.
8.4.2. Example
Generally, if you introduce a JavaFX property field, you will add its corresponding property method along with
setter and getter. In this case, you are advised to bundle them together and document only the field. The "-javafx"
option on the javadoc command will generate the appropriate documentation for the rest of the methods.
N.B. - You can document an individual method in case you want to add explicit information for the method.
/**
* Specifies whether this {@code Node} and its child nodes should be rendered
* as part of the scene graph. A node may be visible and yet not be shown
* in the rendered scene if, for instance, it is off the screen or obscured
* by another Node. Invisible nodes never receive mouse events or
* keyboard focus and never maintain keyboard focus when they become
* invisible.
*
* @defaultValue true
*/
private BooleanProperty visible = new SimpleBooleanProperty(this, "visible",
true);
147
Dec 31, 2016 9:11:14 AM com.sun.javafx.binding.SelectBinding$SelectBindingHelper
getObservableValue
WARNING: Exception while evaluating select-binding [taxable]
"taxable" is a Boolean property on a POJO. The expression that caused this message is the following.
btnCalcTax.disableProperty().bind(
tblItems.getSelectionModel().selectedItemProperty().isNull().or(
Bindings.select(
tblItems.getSelectionModel().selectedItemProperty(),
"taxable"
).isEqualTo(false)
)
);
The preceding statement disables the Calc Tax Button when there is no table selection or if there is a table
selection, the selected item has its taxable field set to false. That is, the Calc Tax Button should only be enabled
when a taxable item is selected.
JavaFX Bindings uses Java Logging, so raising the verbosity to SEVERE will ignore the WARNING level
message. Conversely, if you want to lower the verbosity to see the stack trace supporting the WARNING, lower
the verbosity to FINE. This statement is added to a logging.properties file. The program can be instructed to use
that file by specifying -Djava.util.logging.config.file in the command.
javafx.beans.level=SEVERE
For a quick check that does not require a separate file or command modification, you can add this to your
program. Because of the dependency on Sun classes, you should remove this prior to committing.
Logging.getLogger().setLevel(PlatformLogger.Level.FINE )
The JavaFX WARNING may be too strong for a common use case. There is a ticket JDK-8162600 that may
address this in a later JavaFX release.
JavaFX has a collection of static methods in the Bindings class to work with both POJOs and JavaFX-enabled
Properties. This section demonstrates the use of the select() method which will link the core data types from a
POJO to the JavaFX Property-based fields of a UI control. Once the core data type is repackaged as a Property,
additional functionality from JavaFX can be chained such as string concantenation.
Because the data in this demonstration app is based on a POJO, an update needs to be made manually. Bi-
directional binding works only when the source and the target are both Properties. This app uses a Track class
with core data type fields like String: "album". If the Track class were written using JavaFX properties — say
StringProperty : album — then a model change would automatically update the UI. A hybrid approach is
presented whereby the core data type on one of the POJO fields initializes a separate JavaFX Property field and
148
the update operation must change both fields.
Track.java (abbreviated)
Rating is a pairing of a value (ex, 3) and a scale (ex, max value of 5). There is a Rating member in a Track which
will show the Bindings.select() nesting syntax.
Rating.java (abbreviated)
The construtor, getters, and setters have been left off for brevity and are included in the source download.
149
In the Application subclass, the model is a single field "currentTrack".
BindingsSelectApp.java
Referring to the previous screenshot, the currentTrack fields are displayed in the TextFields(). "rating" is
supplemented with a formatted String.
8.6.2. UI Code
The TextField controls and the Download Button are also Application subclass fields so that they can be
used in both the Application.start() method and a private initBindings() method.
BindingsSelectApp.java (cont.)
"downloaded" is a special JavaFX Property maintained alongside the field of the same name in the currentTrack
object. As mentioned earlier, the POJO will need to be updated manually. The BooleanProperty "downloaded" is
a convenience that keeps the app from having to modify the tfDownload TextField directly.
The start() method begins by creating the top GridPane container and adding the TextField and Label
controls.
150
BindingsSelectApp.java (cont.)
@Override
public void start(Stage primaryStage) throws Exception {
gp.setHgap(4.0d);
gp.setVgap(8.0d);
VBox.setVgrow(gp, Priority.ALWAYS);
VBox.setMargin( gp, new Insets(40.0d) );
A ButtonBar container is used to hold the Download Button. The ButtonBar and GridPane are added to a
VBox and separated via a Separator.
BindingsSelectApp.java (cont.)
ButtonBar.setButtonData(downloadButton, ButtonBar.ButtonData.OTHER);
buttons.getButtons().add(downloadButton);
buttons.setPadding(new Insets(10.0d) );
8.6.3. Bindings
The bindings statements are in a private method "initBindings" which is called from the Application.start()
method.
151
BindingsSelectApp.java (cont.)
tfTrackNo.textProperty().bind(
Bindings.select(currentTrack, "trackNo").asString()
);
tfRating.textProperty().bind(
Bindings.concat(
Bindings.select(currentTrack, "rating", "value").asString(),
" out of ",
Bindings.select(currentTrack, "rating", "scale").asString()
)
);
tfDownloaded.textProperty().bind(downloaded.asString());
downloadButton.disableProperty().bind(downloaded);
}
Bindings.select
Bindings.select is a static method that creates a typed binding from a plain object or an ObservableValue. In this
example, a POJO is passed in along with either a field name or a set of field names that form an object path. For
the artist, album, and track fields, the value returned from select() is a StringBinding and is compatible with the
textProperty() of the TextFields. The trackNo select() call will return an IntegerBinding which is not compatible
with textProperty() so a method asString() is added to convert the IntegerBinding into a StringBinding.
asString() is also used for the special "downloaded" member variable which returns a BooleanBinding that
throws a ClassCastException.
tfRating is bound to a complex expression. The components of tfRating are the value (ex "4.99") and the scale
("5.0"). A string constant " out of " is set in the middle of the expression. The joining of expressions is performed
by the contact() method which returns the formatted string displayed in the UI. Notice that this select() call uses
a path which is a varargs collection of field names. Passing "rating" then "value" is used for the object path
currentTrack.rating.value. currentTrack.rating.scale is accessed similarly.
There is an additional binding disabling the Downloaded Button if the track has already been downloaded. Like
the binding to tfDownloaded, the Bindings.select() method is skipped for a direct bind() call since the member
variable "downloaded" is a JavaFX Property. This "downloaded" Property is initialized with the POJO value
which is a field on the currentTrack object.
Model Update
Since the model is based on the POJO "currentTrack", the fields must be updated manually. In some
architectures, this is desired since the POJO represents record state (the state of the data from the app) and not
the screen state (what the user is looking at). This means that a deliberate setter must be made to update record
state and that needs to trigger a screen state update.
152
Figure 66. A Model Change Disables the Download Button
In this example, there is only one field that will be modified: downloaded. Pressing the Download Button will
call a setter on the special downloaded BooleanProperty of the Application. When the value of the
BooleanProperty changes, the UI is updated from the tfDownloaded binding. A ChangeListener is attached to the
downloaded BooleanProperty which triggers an update of the model state.
BindingsSelectApp.java (cont.)
The Download Button serves as a commit. While the user is limited in this app, they could edit the TextFields
and use a Save Button to transfer the values on the screen to the model. Additionally, a Reset Button could
discard TextField changes.
The declarative binding of JavaFX UI controls to Properties enforces a consistent style and behavior throughout
the application. Similar functionality can be accomplished by directly accessing the controls ("setText()") and
retrieving updates from addListener(). However, listeners are often written inconsistently by different developers
and do not take advantage of the extra binding functions like contact(), sum(), and when(). Bindings.select()
provides a way to bring POJOs used elsewhere in the app into JavaFX.
8.6.4. Source
The complete source code and Gradle project can be found at the link below.
153