JavaFX Spinner for Numbers

The JavaFX version of the popular up-down control is called Spinner. Like its Swing progenitor JSpinner, this control is much more flexible than a typical numerical up-down control. Spinner is designed for arbitrary sequences of objects, with number ranges constituting merely a special case.

This has some unfortunate consequences when you do wish to make an ordinary Spinner for a range of numbers. Here I’ll describe how to access properties that are specific to numerical spinners, and how to improve their direct editing experience.

Numerical Properties

Once you have created a Spinner<Double> or Spinner<Integer> with the conveniently provided special constructors, how do you access its minimum, maximum, and stepping values? The answer is that you have to go through its SpinnerValueFactory which however does not itself define these values. You must first cast it to one of two specialized nested classes, namely DoubleSpinnerValueFactory or IntegerSpinnerValueFactory. Here are some examples:

Spinner<Double> dblSpinner = new Spinner<>(0.0, 10.0, 0.0, 1.0);
SpinnerValueFactory.DoubleSpinnerValueFactory dblFactory =
        (SpinnerValueFactory.DoubleSpinnerValueFactory) dblSpinner.getValueFactory();
double dmin = dblFactory.getMin(); // 0.0
double dmax = dblFactory.getMax(); // 10.0
double dstep = dblFactory.getAmountToStepBy(); // 1.0

Spinner<Integer> intSpinner = new Spinner<>(0, 10, 0, 1);
SpinnerValueFactory.IntegerSpinnerValueFactory intFactory =
        (SpinnerValueFactory.IntegerSpinnerValueFactory) intSpinner.getValueFactory();
int imin = intFactory.getMin(); // 0
int imax = intFactory.getMax(); // 10
int istep = intFactory.getAmountToStepBy(); // 1

Note that min, max, and amountToStepBy are implemented as properties. You can set as well as get their values on the corresponding factory. It’s unfortunate that the generalized object model of Spinner prevented SpinnerValueFactory from directly implementing these properties. I would have welcomed at least getter methods that could throw an exception if the underlying property does not exist. Currently the way to access these fundamental values is both quite verbose and hard to discover.

Direct Editing

All spinners provide an editable property that defaults to false but may be set to true in order to allow the user to directly type input into the TextField that shows the spinner’s current value. For numerical spinners, the required conversion between String and double or int is performed by DoubleStringConverter or IntegerStringConverter, respectively, as soon as the user hits Enter or Tab to commit an edit.

These default converters are rather minimalistic and simply throw a NullPointerException on an empty field, or a NumberFormatException when the user commits any text that cannot be parsed as a valid number. Moreover, the Spinner class itself performs no early validation whatsoever on its TextField input. A screenshot of my spinner demo program (described below) catches and shows the exception after typing a b c Enter into an editable spinner that only accepts integers:

Spinner Demo

This is pretty miserable, so I wrote two custom converter classes to improve the editing experience for numerical spinners. You can download them along with a small demo application as SpinnerDemo.zip which contains a project for IntelliJ IDEA 2017.3.4 and Java SE 9.0.4, along with an executable JAR file and Javadoc. The JAR requires Java 9 but the source code only uses Java SE 8u40 features, so you can recompile on Java 8 if necessary.

The demo application shows four spinners: one each for Integer and Double values, both with the standard converters and my custom converters. The latter will preemptively reject invalid characters while typing, and also show tooltips with the legal input range on the associated spinners. The converters can be used on naked TextField controls or without any controls, too.

Note that when building on-the-fly TextField validation, any text modifications that your validator performs from within the change listener must be wrapped in Platform.runLater calls. Otherwise you’ll get unsupported reentrancy that may cause a mysterious “IllegalArgumentException: The start must be <= the end” while deleting or replacing text. See the source code for how this works.

Java 9 and Bugfixes (2018-02-13)

Version 1.0.2 of SpinnerDemo has been recompiled with Java SE 9 and fixes two bugs in both converter classes. First, the TextField is now correctly reset to its default content when the user commits an empty field, as already documented.

Second, JavaFX 9 throws an exception when attempting to set the ToolTip on a spinner’s TextField because that property is already bound to that of the spinner parent. I don’t know whether JavaFX 9 added the binding or the exception, but at any rate the code now checks whether the ToolTip property is already bound before attempting to set it.

5 thoughts on “JavaFX Spinner for Numbers”

  1. The first attempt at writing a spinner control for JavaFX resulted in what became the ListSpinner in JFXtras. It ended up as a step-through-a-list spinner, the reason for that is that that is what was needed for the CalendarPicker; a list of all months in a year. But the list can also be a fake list, implementing the list API on top of a number and step (for the years in the CalendarPicker). If you have a fixed range, a list based API can be the easier approach.

    1. Thanks for the background info. No doubt the existing Spinner control is great for lists or small ranges, such as months or years. I needed to step through a numerical range at high precision, though, e.g. 0-100 in 0.01 steps. That requires making the TextField editable (who wants to click up arrow a thousand times?), and for that better numerical converters are required. Hopefully they will be built into JavaFX in a future release.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.