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.
Once you have created a
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
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
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.
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
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:
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
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.