JavaFX improvements in Java SE 8u40

how-to
Oct 11, 201534 mins

Learn about JavaFX 8u40's support for accessibility, standard and custom dialogs, a spinner control, and text formatting

Oracle’s Java SE 8u40 update release notes document identifies several new JavaFX features in its discussion of this update’s various enhancements and bug fixes. The new features include accessibility support, standard and custom dialogs, a spinner control, and support for text formatting. In this post, I introduce you to these features.

Accessibility

Q: What is accessibility?

A: Accessibility is the degree to which a product, device, service, or environment is available to as many people as possible. For example, graphical user interfaces can be made more accessible by providing screen reader devices, which speak graphical content, to blind people.

Q: In what ways does JavaFX support accessibility?

A: JavaFX supports accessibility on Windows and Mac OS X platforms in the following ways:

  • By providing support for reading JavaFX controls via a screen reader (e.g., Apple’s VoiceOver for the Mac, Microsoft’s Narrator for Windows, Freedom Scientific’s JAWS for Windows)
  • By providing JavaFX controls that are traversable using the keyboard
  • By supporting a special high-contrast mode that makes controls more visible to users — this mode is activated on Windows by pressing Alt+Left Shift+Print Screen

Q: How does JavaFX support accessibility?

A: JavaFX supports accessibility by adding the following properties to the javafx.scene.Node class:

  • accessibleRoleProperty: the node’s accessible role, which is a value of the javafx.scene.AccessibleRole enumerated type that identifies the kind of control to the screen reader (e.g., AccessibleRole.BUTTON or AccessibleRole.MENU). The screen reader uses a node’s role to identify supported attributes and actions.
  • accessibleRoleDescriptionProperty: the node’s role description, which is a string. Normally, when a role is provided for a node, the screen reader speaks the role along with the node’s contents. When this value is set, it’s possible to override the default, which is useful because the set of roles is predefined. For example, it’s possible to set a node’s role to a button while having the role description be arbitrary text.
  • accessibleTextProperty: the node’s accessible text, which is a string. This property identifies the text that the screen reader will speak. If a node normally speaks text, that text is overridden. For example, a button usually speaks using the text in the control but will no longer do so when this property is set. This property is especially useful for non-text-oriented controls such as image view.
  • accessibleHelpProperty: the node’s accessible help, which is a string. The help text provides a more detailed description of the node’s accessible text. By default, if the node has a tool tip, this text is used.

Additionally, javafx.scene.control.Label‘s labelFor property is used by a screen reader to provide more information to the user. For example, suppose a text field contains Joe and labelFor connects this text field to a label that states First Name. When the text field receives focus, the screen reader will utter “First name, editable text, Joe”.

Finally, JavaFX provides a more advanced API for interacting with the screen reader and accessing accessibility state. You would use this API to introduce accessibility support to your own controls or other nodes.

The advanced API includes the javafx.scene.AccessibleAction and javafx.scene.AccessibleAttribute enumerated types along with the following Node methods:

  • Object queryAccessibleAttribute(AccessibleAttribute attribute, Object... parameters)
  • void executeAccessibleAction(AccessibleAction action, Object... parameters)
  • void notifyAccessibleAttributeChanged(AccessibleAttribute attributes)

An exploration of the advanced API is beyond the scope of this article.

Q: I would like to try out JavaFX’s accessibility support. Can you provide a simple JavaFX application that lets me try out and experiment with accessibility?

A: I’ve created a TempVerter application for converting a temperature to Fahrenheit or Celsius. Listing 1 presents its source code.

Listing 1. TempVerter.java

import javafx.application.Application;

import javafx.event.ActionEvent;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

import javafx.scene.layout.HBox;

import javafx.stage.Stage;

public class TempVerter extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      TextField txtDegrees = new TextField();
      Label lblDegrees = new Label("Degrees:");
      lblDegrees.setLabelFor(txtDegrees);
      Button btnToC = new Button("To C");
      btnToC.setOnAction(ae -> 
                         {
                            try
                            {
                               String input = txtDegrees.getText();
                               double deg = Double.parseDouble(input);
                               txtDegrees.setText("" + ((deg - 32) * 5 / 9.0));
                            }
                            catch (NumberFormatException nfe)
                            {
                               new Alert(AlertType.ERROR, 
                                         "Valid number expected").showAndWait();
                            }
                         });
      Button btnToF = new Button("To F");
      btnToF.setOnAction(ae -> 
                         {
                            try
                            {
                               String input = txtDegrees.getText();
                               double deg = Double.parseDouble(input);
                               txtDegrees.setText("" + (deg * 9 / 5.0 + 32));
                            }
                            catch (NumberFormatException nfe)
                            {
                               new Alert(AlertType.ERROR, 
                                         "Valid number expected").showAndWait();
                            }
                         });
      HBox hboxForm = new HBox(10);
      hboxForm.setPadding(new Insets(10, 10, 10, 10));
      hboxForm.getChildren().addAll(lblDegrees, txtDegrees, btnToC, btnToF);
      Scene scene = new Scene(hboxForm);
      primaryStage.setScene(scene);
      primaryStage.setResizable(false);
      primaryStage.setTitle("TempVerter");
      primaryStage.show();
   }
}

Listing 1 describes an application whose user interface consists of a label, a text field, and a pair of buttons. The application is pretty basic so I don’t have much to say about the source code. However, you will notice the creation of a pair of Alert-based dialogs in the catch handlers of the lambdas associated with the button onAction properties. These dialogs are another new JavaFX feature, which I’ll discuss in the next section.

Compile Listing 1 as follows:

javac TempVerter.java

Before running the application, you need to install a screen reader. For my 64-bit Windows 7 platform, I’m using Microsoft’s pre-installed Narrator software. To run this program, I enter narrator in the Start menu’s Search programs and files text field. In response, I observe the window shown in Figure 1. I also hear a feminine voice describing the window and its contents.

Figure 1. Narrator presents a configuration window for choosing various preferences

Narrator presents a configuration window for choosing various preferences

Switch away from this window and run TempVerter as follows:

java TempVerter

Figure 2 presents this application’s user interface.

Figure 2. As well as describing the window, Narrator speaks the digits as they are entered

As well as describing the window, Narrator speaks the digits as they are entered

Because of lblDegrees.setLabelFor(txtDegrees);, Narrator speaks “Degrees, editable text” instead of “editable text” when the text field has the focus. Comment out this code and listen to the result. You should not hear the computer voice utter “Degrees”.

While experimenting, insert the following line of code after lblDegrees.setLabelFor(txtDegrees);:

lblDegrees.setAccessibleText("Enter temperature in degrees.");

This time, you should hear “Enter temperature in degrees” instead of “Degrees”.

Q: Can you provide me with more information about JavaFX’s support for accessibility?

A: Check out The New JavaFX Accessibility API, which is a video that provides a complete presentation on JavaFX’s accessibility support.

Dialogs

Q: What standard dialogs does JavaFX support?

A: JavaFX supports alert, choice, and text-input standard dialogs. An alert dialog presents a confirmation, error, information, or warning message. A choice dialog presents a list of items and allows at most one of these items to be chosen. A text-input dialog presents a text-input control and solicits the input of text.

Q: How are alert dialogs supported?

A: JavaFX supports alert dialogs via the javafx.scene.control.Alert class and its nested AlertType enum.

Q: Can you present a JavaFX application that demonstrates the various alert dialogs?

A: Listing 1 revealed the use of Alert for presenting an error dialog. Listing 2 presents an application that demonstrates all of the possibilities.

Listing 2. AlertDemo.java

import java.util.Optional;

import javafx.application.Application;
import javafx.application.Platform;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;

import javafx.scene.layout.HBox;

import javafx.stage.Stage;

public class AlertDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      Button btn1 = new Button("Demo 1");
      btn1.setOnAction(ae ->
                       {
                         // 1. Confirmation dialog with header message

                         Alert alert = new Alert(AlertType.CONFIRMATION);
                         alert.setTitle("Confirmation");
                         alert.setHeaderText("Exit application");
                         alert.setContentText("Do you want to exit the " +
                                              "application?");
                         Optional<ButtonType> result = alert.showAndWait();
                         if (result.get() == ButtonType.OK)
                            Platform.exit();
                       });
      Button btn2 = new Button("Demo 2");
      btn2.setOnAction(ae ->
                       {
                         // 2. Confirmation dialog without header message

                         Alert alert = new Alert(AlertType.CONFIRMATION);
                         alert.setTitle("Confirmation");
                         alert.setContentText("Do you want to exit the " +
                                              "application?");
                         Optional<ButtonType> result = alert.showAndWait();
                         if (result.get() == ButtonType.OK)
                            Platform.exit();
                       });
      Button btn3 = new Button("Demo 3");
      btn3.setOnAction(ae ->
                       {
                         // 3. Error dialog with header message

                         Alert alert = new Alert(AlertType.ERROR);
                         alert.setTitle("Error");
                         alert.setHeaderText("Printer error");
                         alert.setContentText("Paper not loaded");
                         alert.showAndWait();
                       });
      Button btn4 = new Button("Demo 4");
      btn4.setOnAction(ae ->
                       {
                         // 4. Error dialog without header message

                         Alert alert = new Alert(AlertType.ERROR);
                         alert.setTitle("Error");
                         alert.setContentText("Paper not loaded");
                         alert.showAndWait();
                       });
      Button btn5 = new Button("Demo 5");
      btn5.setOnAction(ae ->
                       {
                         // 5. Information dialog with header message

                         Alert alert = new Alert(AlertType.INFORMATION);
                         alert.setTitle("Information");
                         alert.setHeaderText("Time status");
                         alert.setContentText("The time is currently 10PM.");
                         alert.showAndWait();
                       });
      Button btn6 = new Button("Demo 6");
      btn6.setOnAction(ae ->
                       {
                         // 6. Information dialog without header message

                         Alert alert = new Alert(AlertType.INFORMATION);
                         alert.setTitle("Information");
                         alert.setContentText("The time is currently 10PM.");
                         alert.showAndWait();
                       });
      Button btn7 = new Button("Demo 7");
      btn7.setOnAction(ae ->
                       {
                         // 7. Warning dialog with header message

                         Alert alert = new Alert(AlertType.WARNING);
                         alert.setTitle("Warning");
                         alert.setHeaderText("Battery status");
                         alert.setContentText("The battery charge is low.");
                         alert.showAndWait();
                       });
      Button btn8 = new Button("Demo 8");
      btn8.setOnAction(ae ->
                       {
                         // 8. Warning dialog without header message

                         Alert alert = new Alert(AlertType.WARNING);
                         alert.setTitle("Warning");
                         alert.setContentText("The battery charge is low.");
                         alert.showAndWait();
                       });
      HBox hboxForm = new HBox(10);
      hboxForm.setPadding(new Insets(10, 10, 10, 10));
      hboxForm.getChildren().addAll(btn1, btn2, btn3, btn4, btn5, btn6, btn7, 
                                    btn8);
      Scene scene = new Scene(hboxForm);
      primaryStage.setScene(scene);
      primaryStage.setResizable(false);
      primaryStage.setTitle("Alert Demo");
      primaryStage.show();
   }
}

Listing 2 describes an application whose user interface consists of eight buttons in a horizontal line. Clicking each button demonstrates either the confirmation, error, information, or warning dialog with custom or default header text.

The Alert(Alert.AlertType alertType) constructor is called to initialize an Alert dialog object to a specific alertType. The dialog’s title is specified by calling void setTitle(String title), its header text is specified by calling void setHeaderText(String headerText), and its content text is set by calling void setContentText(String contentText).

The Optional<R> showAndWait() method presents the dialog and waits for a user response before dismissing the dialog. The returned java.util.Optional object is a container that may or may not contain a non-null result value.

Compile Listing 2 as follows:

javac AlertDemo.java

Run the resulting application as follows:

java AlertDemo

Figure 3 reveals the resulting user interface.

Figure 3. The buttons demonstrate the confirmation with custom/default header text, error with custom/default header text, information with custom/default header text, and warning with custom/default header text dialogs

The buttons demonstrate the confirmation with custom/default header text, error with custom/default header text, information with custom/default header text, and warning with custom/default header text dialogs

Click any of the buttons and you’ll see the dialogs shown in Figures 4 through 11, which correspond to the buttons being clicked in a left-to-right sequence.

Figure 4. Presenting a confirmation dialog with custom header text

Presenting a confirmation dialog with custom header text

Figure 5. Presenting a confirmation dialog with default header text

Presenting a confirmation dialog with default header text

Figure 6. Presenting an error dialog with custom header text

Presenting an error dialog with custom header text

Figure 7. Presenting an error dialog with default header text

Presenting an error dialog with default header text

Figure 8. Presenting an information dialog with custom header text

Presenting an information dialog with custom header text

Figure 9. Presenting an information dialog with default header text

Presenting an information dialog with default header text

Figure 10. Presenting a warning dialog with custom header text

Presenting a warning dialog with custom header text

Figure 11. Presenting a warning dialog with default header text

Presenting a warning dialog with default header text

Q: How are choice dialogs supported?

A: JavaFX supports choice dialogs via the javafx.scene.control.ChoiceDialog<T> class. The type passed to T identifies the type of the items to show to the user, and also identifies the type of the object that’s returned from ChoiceDialog<T>‘s T getResult() method when the dialog is dismissed.

Q: Can you present a JavaFX application that demonstrates a choice dialog?

A: Check out Listing 3.

Listing 3. ChoiceDemo.java

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import javafx.application.Application;
import javafx.application.Platform;

import javafx.scene.Scene;

import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.ChoiceDialog;

import javafx.stage.Stage;

public class ChoiceDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      List<String> fruits = new ArrayList<>();
      fruits.add("apples");
      fruits.add("bananas");
      fruits.add("cherries");
      fruits.add("oranges");
      ChoiceDialog<String> choice = new ChoiceDialog<>("bananas", fruits);
      choice.setTitle("Fruits");
      choice.setHeaderText("Fruits");
      choice.setContentText("Choose your fruit:");
      Optional<String> result = choice.showAndWait();
      if (result.isPresent())
      {
         Alert alert = new Alert(AlertType.INFORMATION);
         alert.setContentText(result.get() + " are great!");
         alert.showAndWait();
      }
   }
}

Listing 3 describes an application whose user interface consists of a choice dialog followed by an alert dialog.

The choice dialog is constructed by passing "bananas" and fruits to the ChoiceDialog(T defaultChoice, Collection choices) constructor. After setting the dialog’s title, header text, and content text, the dialog is displayed and the result is returned. If a return value is present, an alert dialog is created with the result text built into the dialog’s message. The alert dialog is then displayed.

Compile Listing 3 as follows:

javac ChoiceDemo.java

Run the resulting application as follows:

java ChoiceDemo

Figure 12 reveals the resulting user interface.

Figure 12. The default bananas text is the initially selected text

The default bananas text is the initially selected text

When you open the drop-down list of fruit names, you observe the dialog shown in Figure 13.

Figure 13. Opening the drop-down list reveals various fruit choices

Opening the drop-down list reveals various fruit choices

After selecting a fruit name and clicking the OK button, you should observe the alert dialog shown in Figure 14.

Figure 14. Revealing a favorite fruit

Revealing a favorite fruit

Q: How are text-input dialogs supported?

A: JavaFX supports text-input dialogs via the javafx.scene.control.TextInputDialog class.

Q: Can you present a JavaFX application that demonstrates a text-input dialog?

A: Check out Listing 4.

Listing 4. TextInputDemo.java

import java.util.Optional;

import javafx.application.Application;
import javafx.application.Platform;

import javafx.scene.Scene;

import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.TextInputDialog;

import javafx.stage.Stage;

public class TextInputDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      TextInputDialog textInput = new TextInputDialog("Enter some text");
      textInput.setTitle("Enter some text");
      textInput.setHeaderText("Text Input");
      textInput.setContentText("Please enter something:");
      Optional<String> result = textInput.showAndWait();
      if (result.isPresent())
      {
         Alert alert = new Alert(AlertType.INFORMATION);
         alert.setContentText("You entered: " + result.get());
         alert.showAndWait();
      }
   }
}

Listing 4 describes an application whose user interface consists of a text-input dialog followed by an alert dialog.

The text-input dialog is constructed by passing "Enter some text" to the TextInputDialog(String defaultValue) constructor. After setting the dialog’s title, header text, and content text, the dialog is displayed and the result is returned. If a return value is present, an alert dialog is created with the result text built into the dialog’s message. The alert dialog is then displayed.

Compile Listing 4 as follows:

javac TextInputDemo.java

Run the resulting application as follows:

java TextInputDemo

Figure 15 reveals the resulting user interface.

Figure 15: The default Enter some text is the initially selected text

The default Enter some text is the initially selected text

Enter some text, such as JavaFX's standard dialogs are a great asset., and click the OK button. The alert dialog shown in Figure 16 reveals this text.

Figure 16: Revealing entered text

Revealing entered text

Q: How do I create custom dialogs?

A: You create custom dialogs by working with the javafx.scene.control.Dialog<R> class, which serves as a wrapper for the javafx.scene.control.DialogPane class in which the real work takes place. (DialogPane knows nothing of the dialog’s underlying implementation, which currently consists of being shown inside a stage, but may also be shown in lightweight or internal dialog contexts in future JavaFX releases. Dialog<R> is simply a wrapper that attempts to present a common API for all possible implementations.) Furthermore, Dialog<R> is the superclass of the Alert, ChoiceDialog<T>, and TextInputDialog classes. Figure 17 illustrates the relationships between these classes.

Figure 17: Relating the classes that support standard and custom dialogs

Relating the classes that support standard and custom dialogs.

The R parameter in Dialog<R> identifies the type of the object that is returned when the dialog is dismissed. This object contains the values that were selected by the user via the dialog’s various controls. For example, a search dialog might contain a text field for entering search text and a check box for selecting whether a search is case-sensitive or not. This object would contain these settings upon dialog dismissal.

The object that is returned upon dialog dismissal is managed by the dialog’s result property. When the dialog is dismissed, a result converter converts the values in the dialog’s controls to the object that is managed by result. The result converter is an instance of a class that implements the javafx.util.Callback<P, R> interface, where P is the type of the argument passed to Callback<P, R>‘s R call(P param) method and R is this method’s return type. The callback is installed by calling Dialog<R>‘s void setResultConverter(Callback<ButtonType, R> value) method. Here, P is set to one of the javafx.scene.control.ButtonType class’s constants (such as OK or CANCEL) and R is the type passed to Dialog<R>. Behind the scenes, the converter callback’s call() method is called when the button whose button type is associated with the converter is clicked.

Additionally, you can call the following methods to customize a dialog:

  • void setGraphic(Node graphic)
  • void setHeaderText(String headerText)
  • void setTitle(String title)

You’ll also need to access the wrapped DialogPane and you can do so by calling Dialog<R>‘s DialogPane getDialogPane() method. You would then call DialogPane methods such as ObservableList<ButtonType> getButtonTypes() (obtain an observable list of the dialog’s button types so that you can specify the buttons that are displayed by the dialog) and void setContent(Node content) (install the dialog’s controls as a node hierarchy).

Q: Can you present a JavaFX application that clarifies this discussion?

A: Listing 5 describes an application that displays a search dialog box, which solicits search text and a case-sensitive search true/false value. When the Search button is clicked, these values are output the standard output stream.

Listing 5. SearchDialog.java

import java.util.Optional;

import javafx.application.Application;
import javafx.application.Platform;

import javafx.geometry.Insets;

import javafx.scene.Node;

import javafx.scene.control.ButtonBar.ButtonData;
import javafx.scene.control.ButtonType;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

import javafx.scene.image.ImageView;

import javafx.scene.layout.GridPane;

import javafx.stage.Stage;

import javafx.util.Pair;

public class SearchDialog extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      // Create the custom dialog.

      Dialog<Pair<String, Boolean>> dialog = new Dialog<>();
      dialog.setTitle("Search");
      dialog.setHeaderText("Enter search parameters");

      // Set the search icon.

      dialog.setGraphic(new ImageView(this.getClass().getResource("search.png").
                                      toString()));

      // Set the button types.

      ButtonType searchButtonType = new ButtonType("Search", ButtonData.OK_DONE);
      dialog.getDialogPane().getButtonTypes().addAll(searchButtonType, 
                                                     ButtonType.CANCEL);

      // Create the layout for the controls.

      GridPane grid = new GridPane();
      grid.setHgap(10);
      grid.setVgap(10);
      grid.setPadding(new Insets(20, 150, 10, 10));

      // Create and initialize the search-text and case-sensitive-search controls.

      TextField srchText = new TextField();
      srchText.setPromptText("Search text");
      CheckBox css = new CheckBox("Case-sensitive search");

      // Populate the layout with a label along with the search text and 
      // case-sensitive search controls.

      grid.add(new Label("Search Text:"), 0, 0);
      grid.add(srchText, 1, 0);
      grid.add(css, 0, 1);

      // Disable/enable search button depending on whether search-text field is 
      // empty. Button defaults to being disabled.

      Node searchButton = dialog.getDialogPane().lookupButton(searchButtonType);
      searchButton.setDisable(true);
      srchText.textProperty().addListener((observable, oldValue, newValue) -> 
      {
         searchButton.setDisable(newValue.trim().isEmpty());
      });

      // Install controls layout in the dialog panel.

      dialog.getDialogPane().setContent(grid);

      // Request focus on the search-text field. See 
      // https://community.oracle.com/thread/2321126 for information on why 
      // Platform.runLater() is used.

      Platform.runLater(() -> srchText.requestFocus());

      // Convert the result to a srchtext-css-status pair when the search button
      // is clicked.

      dialog.setResultConverter(dialogButton -> 
      {
         if (dialogButton == searchButtonType)
            return new Pair<>(srchText.getText(), css.isSelected());
         return null;
      });

      // Display dialog box and wait for user response.

      Optional<Pair<String, Boolean>> result = dialog.showAndWait();

      // If the user closed the dialog box via the search button, output the 
      // chosen search text and case-sensitive search status.

      result.ifPresent(stcss -> 
      {
         System.out.println("Search text = " + stcss.getKey() + 
                            ", Case-sensitive search = " + stcss.getValue());
      });
   }
}

Listing 5 uses the java.util.Pair<K, V> class to conveniently store a pair of items: the search text and the Boolean case-sensitive-search value. A result converter has been installed to convert the dialog’s search text field and case-sensitive search check box values to a Pair<K, V> object that is returned from the call() method. This conversion happens only when the Search button is clicked; it doesn’t happen when Cancel is clicked.

Compile Listing 5 as follows:

javac SearchDialog.java

Run the resulting application as follows:

java SearchDialog

Figure 18 reveals the resulting dialog.

The search dialog defaults to no search text and a case-insensitive search

Suppose you enter JavaFX in the text field and check the check box. After clicking Search, you should observe the following output on the standard output stream:

Search text = JavaFX, Case-sensitive search = true

Q: Can you provide me with more information about JavaFX’s support for standard and custom dialogs?

A: Check out JavaFX Dialogs for additional standard and custom dialog examples (including a custom login dialog), and to learn about dialog styling and setting the dialog owner and modality.

Spinner control

Q: What is a spinner?

A: A spinner is a single-line text field control that lets the user select a number or an object value from an ordered sequence of such values. Spinners typically provide a pair of tiny arrow buttons for stepping through the elements of the sequence. The keyboard’s up arrow/down arrow keys also cycle through the elements. The user may also be allowed to type a (legal) value directly into the spinner. Although combo boxes provide similar functionality, spinners are sometimes preferred because they don’t require a drop-down list that can obscure important data, and also because they allow for features such as wrapping from the maximum value back to the minimum value (e.g., from the largest positive integer to 0).

Q: How is a spinner implemented?

A: A spinner is implemented as an instance of the javafx.scene.control.Spinner control class. This class relies on the abstract javafx.scene.control.SpinnerValueFactory class to provide the control’s model (range of user-selectable values of a specific type). Currently, the only supported models are defined by its nested DoubleSpinnerValueFactory, IntegerSpinnerValueFactory, and ListSpinnerValueFactory classes.

Q: How do I create a spinner?

A: You create a spinner by calling one of Spinner‘s constructors. For example, Spinner(int min, int max, int initialValue) creates a spinner for selecting one of the integer values from min through max. The initially selected value is identified by initialValue. If this value isn’t in the min/max range, min‘s value becomes the initially selected value.

Spinner(int min, int max, int initialValue) is a convenience constructor that installs an instance of the IntegerSpinnerValueFactory class with these values as the model. If you prefer to work directly with IntegerSpinnerValueFactory, you can instantiate this class and pass its reference to the Spinner(SpinnerValueFactory<T> valueFactory) constructor. Alternatively, you can create an empty spinner via the Spinner() constructor and invoke Spinner‘s void setValueFactory(SpinnerValueFactory<T> value) method to install this factory object.

Q: Can you provide a simple example of the integer and double precision floating-point spinners?

A: Check out Listing 6.

Listing 6. SpinnerDemo.java (version 1)

import javafx.application.Application;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.Spinner;

import javafx.scene.layout.GridPane;

import javafx.stage.Stage;

public class SpinnerDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      Spinner<Integer> ispinner = new Spinner<>(1, 10, 2);
      Spinner<Double> dspinner = new Spinner<>(1.5, 3.5, 1.5, 0.5);

      GridPane grid = new GridPane();
      grid.setHgap(10);
      grid.setVgap(10);
      grid.setPadding(new Insets(10));

      grid.add(new Label("Integer Spinner"), 0, 0);
      grid.add(ispinner, 1, 0);
      grid.add(new Label("Double Spinner"), 0, 1);
      grid.add(dspinner, 1, 1);

      Scene scene = new Scene(grid, 350, 100);
      primaryStage.setTitle("SpinnerDemo");
      primaryStage.setScene(scene);
      primaryStage.show();
   }
}

Listing 6’s start() method first creates an integer spinner via the aforementioned constructor. It then creates a double precision floating-point spinner via the Spinner(double min, double max, double initialValue, double amountToStepBy) constructor. This constructor receives the units in which to increment or decrement the spinner via the value passed to amountToStepBy, which happens to be 0.5.

Continuing, start() creates and configures a grid pane container and populates the 2-row-by-2-column grid with these spinners and associated labels. It then creates the scene based on the grid and configures/shows the stage.

Compile Listing 6 as follows:

javac SpinnerDemo.java

Run the resulting application as follows:

java SpinnerDemo

Figure 19 reveals the resulting user interface.

Figure 19: Click each spinner’s arrows to increment/decrement through the range of values

Click each spinner's arrows to increment/decrement through the range of values

Q: I want to make the previous spinners editable. How do I accomplish this task?

A: Call Spinner‘s void setEditable(boolean value) method, passing true to value. I’ve created a second version of SpinnerDemo to demonstrate. See this article’s code archive for the source code.

Q: When I make the previous spinners editable, type illegal characters (such as letters) into the text field, and press the Enter key, an exception is thrown. The exception is java.lang.NumberFormatException for the integer-based spinner and java.lang.RuntimeException (wrapping java.text.ParseException) for the double precision floating-point spinner. How can I prevent this exception from being thrown?

A: You can prevent this exception from being thrown by installing an instance of a subclass of the abstract javafx.util.StringConverter<T> class (where T is the type being converted to or from String) as the SpinnerValueFactory converter. This object would catch the exception being thrown and take action.

Spinner uses a javafx.scene.control.TextField object as an editor for obtaining user input. Because a text field can store any character, inappropriate characters such as letters in a numeric context can be entered. After the user presses Enter, the input is passed to the SpinnerValueFactory converter’s T fromString(String string) method. For the integer or double precision floating-point factories, T is either Integer or Double. Converting from a string with illegal characters to a number results in the exception being thrown from fromString(). Obtain a reference to the current converter and install a new converter whose fromString() method invokes the other converter’s fromString() method in a try statement with an appropriate catch block. Listing 7 presents an application that accomplishes this task.

Listing 7. SpinnerDemo.java (version 3)

import javafx.application.Application;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.Spinner;

import javafx.scene.layout.GridPane;

import javafx.stage.Stage;

import javafx.util.StringConverter;

public class SpinnerDemo extends Application
{
    @Override
    public void start(Stage primaryStage)
    {
        Spinner<Integer> ispinner = new Spinner<>(1, 10, 2);
        ispinner.setEditable(true);
        StringConverter<Integer> sci = ispinner.getValueFactory().getConverter();
        StringConverter<Integer> sci2 = new StringConverter<Integer>()
                                        {
                                           @Override
                                           public Integer fromString(String value)
                                           {
                                              try
                                              {
                                                 return sci.fromString(value);
                                              }
                                              catch (NumberFormatException nfe)
                                              {
                                                 System.out.println("Bad integer: " + 
                                                                    value);
                                                 return 0;
                                              }
                                           }

                                           @Override
                                           public String toString(Integer value)
                                           {
                                              return sci.toString(value);
                                           }
                                        };
        ispinner.getValueFactory().setConverter(sci2);

        Spinner<Double> dspinner = new Spinner<>(1.5, 3.5, 1.5, 0.5);
        dspinner.setEditable(true);
        StringConverter<Double> scd = dspinner.getValueFactory().getConverter();
        StringConverter<Double> scd2 = new StringConverter<Double>()
                                       {
                                          @Override
                                          public Double fromString(String value)
                                          {
                                             try
                                             {
                                                return scd.fromString(value);
                                             }
                                             catch (RuntimeException re)
                                             {
                                                System.out.println("Bad double: " + 
                                                                   value);
                                                System.out.println("Cause: " + 
                                                                   re.getCause());
                                                return 0.0;
                                             }
                                          }

                                          @Override
                                          public String toString(Double value)
                                          {
                                             return scd.toString(value);
                                          }
                                       };
        dspinner.getValueFactory().setConverter(scd2);

        GridPane grid = new GridPane();
        grid.setHgap(10);
        grid.setVgap(10);
        grid.setPadding(new Insets(10));

        grid.add(new Label("Integer Spinner"), 0, 0);
        grid.add(ispinner, 1, 0);
        grid.add(new Label("Double Spinner"), 0, 1);
        grid.add(dspinner, 1, 1);

        Scene scene = new Scene(grid, 350, 100);
        primaryStage.setTitle("SpinnerDemo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}

Compile Listing 7 (javac SpinnerDemo.java) and run the resulting application (java SpinnerDemo). You should observe error messages similar to those shown below when you enter illegal characters into a spinner’s text field:

Bad integer: a2
Bad double: b1.5
Cause: java.text.ParseException: Unparseable number: "b1.5"

Q: I need a string-based spinner. How do I obtain one?

A: You can obtain a string-based spinner via the Spinner(ObservableList<T> items) constructor. For example, the following code fragment shows you how to create a spinner for selecting a weekday name:

List<String> weekDays = Arrays.asList("Monday", "Tuesday", "Wednesday",
                                      "Thursday", "Friday", "Saturday",
                                      "Sunday");
ObservableList<String> obsWeekDays = 
   FXCollections.observableList(weekDays);
Spinner<String> sspinner = new Spinner<>(obsWeekDays);

I excerpted this code fragment from a fourth version of the SpinnerDemo application (see this article’s code archive for the source code). When you run that application, you’ll see the spinner shown in Figure 20.

Figure 20: Attempting to enter text into the editor that doesn’t exactly match one of the weekday names results in java.lang.UnsupportedOperationException being thrown

Attempting to enter text into the editor that doesn't exactly match one of the weekday names results in java.lang.UnsupportedOperationException being thrown.

Q: Can you provide me with more information about JavaFX’s support for spinners?

A: Check out the JavaFX 8 documentation on Spinner and SpinnerValueFactory for more information about this control and its model. Also, you might want to run a Google search to find out how others are using this control.

Text formatting

Q: How does JavaFX support text formatting?

A: JavaFX supports text formatting by providing the javafx.scene.control.TextFormatter<V> class with its nested Change class. Furthermore, the abstract javafx.scene.control.TextInputControl class (the parent class of TextField and javafx.scene.control.TextArea) has been given a textFormatter property so that any subclass automatically supports text formatting.

Q: What kinds of text formatting are supported?

A: TextFormatter supports two kinds of text formatting: value and change. A value formatter is called when you press the Enter key after entering the text. A change formatter is called for every text-deletion, text-addition, and text-replacement change for the focused text-input control. These formatters can be used separately or together.

Q: How are value and change formatters specified?

A: A value formatter is specified as an instance of a StringConverter<T> subclass. A change formatter is specified as an instance of a class that implements the java.util.function.UnaryOperator<T> functional interface with TextFormatter.Change passed to T. A Change instance contains the state representing a change in the content or selection for a TextInputControl subclass object. You override UnaryOperator‘s T apply(T t) method to obtain and take action based on what has changed. The Change instance is returned to commit the change; null is returned to reject the change.

After creating value and change formatters, pass them to an appropriate TextFormatter constructor:

  • TextFormatter(StringConverter<V> valueConverter) (install a value formatter)
  • TextFormatter(UnaryOperator<TextFormatter.Change> filter) (install a change formatter)
  • TextFormatter(StringConverter<V> valueConverter, V defaultValue, UnaryOperator<TextFormatter.Change> filter) (install value and change formatters)

You install the TextFormatter object by calling TextInputControl‘s void setTextFormatter(TextFormatter<?> value) method.

Q: Can you provide an application that demonstrates value formatting?

A: Check out Listing 8.

Listing 8. TextFormatterDemo.java (version 1)

import javafx.application.Application;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;

import javafx.scene.layout.HBox;

import javafx.stage.Stage;

import javafx.util.StringConverter;

public class TextFormatterDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      Label lblAge = new Label("Age");
      TextField txtAge = new TextField("");
      StringConverter<Integer> formatter;
      formatter = new StringConverter<Integer>()
                  {
                     @Override
                     public Integer fromString(String string)
                     {
                        System.out.println("fromString(): string = " + string);
                        return Integer.parseInt(string);
                     }

                     @Override
                     public String toString(Integer object)
                     {
                        System.out.println("toString(): object = " + object);
                        if (object == null)
                           return "0";
                        System.out.println("object.tostring = " + 
                                           object.toString());
                        return object.toString();
                     }
                  };
      txtAge.setTextFormatter(new TextFormatter<Integer>(formatter));
      HBox hboxForm = new HBox(10);
      hboxForm.setPadding(new Insets(10, 10, 10, 10));
      hboxForm.getChildren().addAll(lblAge, txtAge);
      Scene scene = new Scene(hboxForm);
      primaryStage.setScene(scene);
      primaryStage.setResizable(false);
      primaryStage.setTitle("TextFormatterDemo");
      primaryStage.show();
   }
}

Listing 8 describes an application that presents a text field for entering an age. It registers a value formatter with this text field to take appropriate action when the user enters non-digits (although the minus sign can be entered once — I’ll leave it as an exercise to ensure that the minus sign isn’t entered).

The best way to understand how this application works is to try it out. First, compile Listing 8 as follows:

javac TextFormatterDemo.java

Next, run the application as follows:

java TextFormatterDemo

Figure 21 shows the resulting user interface.

Figure 21: A default value of 0 is displayed. (Obviously, a living person can never be zero years old.)

A default value of 0 is displayed. (Obviously, a living person can never be zero years old.)

Additionally, the following messages are output to the standard output stream:

toString(): object = null
toString(): object = null

These messages indicate that the value formatter’s toString() method is called to convert the initial default value, which is null when you call TextFormatter(StringConverter<V> valueConverter) to install the value formatter but can be a non-null reference when you call TextFormatter(StringConverter<V> valueConverter, V defaultValue), to a string.

Next, replace the highlighted 0 with 10 and press Enter. You should observe the following messages on the standard output stream:

fromString(): string = 10
toString(): object = 10
object.tostring = 10

The fromString() method is called to convert the entered string to an integer. If an illegal value had been entered, fromString()‘s Integer.parseInt(string) method would have thrown NumberFormatException and the text formatter would have replaced what was entered with the previous legal value. This would have necessitated converting that value to a string. Either way, toString() is called to perform the conversion.

If you were to type 10a instead of 10 and press Enter, you would observe the text field reverting to 0 and the following messages:

fromString(): string = 10a
toString(): object = null

fromString()‘s parseInt() call results in NumberFormatException being thrown. This causes toString() to be called with the previously valid value, which happens to be null because there is no default. When null is seen, toString() returns "0" as the default, which is why you initially see 0 in the text field and also after entering 10a.

I’ve created a second version of this application (see the code archive) that demonstrates a default value. I’ve excerpted the relevant code below:

txtAge.setTextFormatter(new TextFormatter<Integer>(formatter, 1));

This code installs a value formatter with a default value of 1.

Q: Can you provide an application that demonstrates change formatting?

A: Check out Listing 9.

Listing 9. TextFormatterDemo.java (version 3)

import java.util.function.UnaryOperator;

import javafx.application.Application;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;

import javafx.scene.layout.HBox;

import javafx.stage.Stage;

public class TextFormatterDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      Label lblPhone = new Label("Phone");
      TextField txtPhone = new TextField("555-5555");
      UnaryOperator<TextFormatter.Change> filter;
      filter = new UnaryOperator<TextFormatter.Change>()
               {
                  @Override
                  public TextFormatter.Change apply(TextFormatter.Change change)
                  {
                     System.out.println(change);
                     String text = change.getText();
                     for (int i = 0; i < text.length(); i++)
                        if (!Character.isDigit(text.charAt(i)))
                          return null;
                     return change;
                  }
               };
      txtPhone.setTextFormatter(new TextFormatter<String>(filter));
      HBox hboxForm = new HBox(10);
      hboxForm.setPadding(new Insets(10, 10, 10, 10));
      hboxForm.getChildren().addAll(lblPhone, txtPhone);
      Scene scene = new Scene(hboxForm);
      primaryStage.setScene(scene);
      primaryStage.setResizable(false);
      primaryStage.setTitle("TextFormatterDemo");
      primaryStage.show();
   }
}

Listing 9: TextFormatterDemo.java (version 3)

Listing 9 describes an application that presents a text field for entering a phone number. It registers a change formatter with this text field to take appropriate action when the user enters non-digits. Because UnaryOperator is a functional interface, I could have used a lambda instead of instantiating an anonymous class that implements an interface. I decided on the latter approach for clarity. If you would like a lambda example, check out Java 8 U40 TextFormatter (JavaFX) to restrict user input only for decimal number.

Whenever you press a key while the text field has the focus, the apply() method is called. It outputs the change argument to the standard output stream, obtains the text that has changed, and scans it for a non-digit. If a non-digit is seen, null is returned to abort the change. Otherwise, change is returned to accept the change.

Compile Listing 9 (javac TextFormatterDemo.java) and run the resulting application (java TextFormatterDemo). Figure 22 shows the resulting user interface.

Figure 22: A default value of 555-5555 is displayed

A default value of 555-5555 is displayed

If you type a non-digit apart from the backspace or Delete, nothing will happen.

Q: Can you provide an application that combines value and change formatting?

A: Check out Listing 10.

Listing 10. TextFormatterDemo.java (version 4)

import java.util.function.UnaryOperator;

import javafx.application.Application;

import javafx.geometry.Insets;

import javafx.scene.Scene;

import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;

import javafx.scene.layout.HBox;

import javafx.stage.Stage;

import javafx.util.StringConverter;

public class TextFormatterDemo extends Application
{
   @Override
   public void start(Stage primaryStage)
   {
      Label lblPhone = new Label("Phone");
      TextField txtPhone = new TextField("555-5555");
      StringConverter<String> formatter;
      formatter = new StringConverter<String>()
                  {
                     @Override
                     public String fromString(String string)
                     {
                        System.out.println("fromString(): string = " + string);
                        if (string.length() == 8)
                           return string;
                        else
                        if (string.length() == 7 && string.indexOf('-') == -1)
                           return string.substring(0, 3) + "-" + 
                                  string.substring(3);
                        else
                           return "333-3333";
                     }

                     @Override
                     public String toString(String object)
                     {
                        System.out.println("toString(): object = " + object);
                        if (object == null)   // only null when called from 
                           return "777-7777"; // TextFormatter constructor 
                                              // without default
                        System.out.println("object.tostring = " + 
                                           object.toString());
                        return object;
                     }
                  };
      UnaryOperator<TextFormatter.Change> filter;
      filter = new UnaryOperator<TextFormatter.Change>()
               {
                  @Override
                  public TextFormatter.Change apply(TextFormatter.Change change)
                  {
                     System.out.println(change);
                     String text = change.getText();
                     for (int i = 0; i < text.length(); i++)
                        if (!Character.isDigit(text.charAt(i)))
                          return null;
                     return change;
                  }
               };
      txtPhone.setTextFormatter(new TextFormatter<String>(formatter, "555-5555",
                                                          filter));
      HBox hboxForm = new HBox(10);
      hboxForm.setPadding(new Insets(10, 10, 10, 10));
      hboxForm.getChildren().addAll(lblPhone, txtPhone);
      Scene scene = new Scene(hboxForm);
      primaryStage.setScene(scene);
      primaryStage.setResizable(false);
      primaryStage.setTitle("TextFormatterDemo");
      primaryStage.show();
   }
}

As with Listing 9, Listing 10 describes an application that presents a text field for entering a phone number. As well as preserving Listing 9’s change formatter, Listing 10 adds a value formatter. The value formatter ensures that the seven-digit phone number remains hyphenated when Enter is pressed.

Compile Listing 10 (javac TextFormatterDemo.java) and run the resulting application (java TextFormatterDemo). You should observe the same user interface as shown in Figure 22.

With 555-5555 highlighted, press Delete and then Enter. You should observe 333-3333 in the text field. Press the backspace key once. You should now observe 333-333. Press Enter and the erroneous number is still displayed. You will have to switch to another application and return to this application to see the value replaced by 333-3333.

Erase the contents of the text field and enter 1234567. Press Enter and you should observe 123-4567 in the text field.

What’s next?

Next time, I’ll present the infrastructure for populating a checkerboard with multiple checkers and dragging them around the board, making sure to center a dragged checker on its target square.

download
Get the source code for this post’s applications. Created by Jeff Friesen for JavaWorld

The following software was used to develop the post’s code:

  • 64-bit JDK 8u40

The post’s code was tested on the following platform(s):

  • JVM on 64-bit Windows 7 SP1