The approach of using a listener and reverting to valid values if invalid values are input by the user will work, but it can create issues if you have other listeners on the text field's textProperty
. Those listeners will observe the invalid values as well as the valid ones, so they have to know to filter out any invalid values.
A better approach is to use a TextFormatter
. The TextFormatter
can do two things:
- Define a "Filter", which can veto, or modify, any changes made to the
TextField
's text
- Define a "converter", which defines how to convert the text to and from values of any specific type (e.g.
Double
) in your case.
Defining the appropriate filter can be tricky: you want to allow any reasonable edits by the user. This means the text may be in an invalid state while the user is editing; e.g. you probably want to allow the text field to be completely empty, even though that does not represent a valid value. (Otherwise, it becomes annoying to the user if, for example, they want to change "1" to "2".) Similarly you probably want to allow things like "-" and ".", etc.
Here is an example. The filter should modify the change that is passed to it, if needed, and can return null
to completely veto a change. This example simply checks if the text represents a valid editing state, and returns the change unmodified if it does, vetoing it otherwise. The formatter needs to deal with any text that is allowed by the filter and convert it to a double. Here anything that is incomplete is just represented as zero.
Pattern validEditingState = Pattern.compile("-?(([1-9][0-9]*)|0)?(\.[0-9]*)?");
UnaryOperator<TextFormatter.Change> filter = c -> {
String text = c.getControlNewText();
if (validEditingState.matcher(text).matches()) {
return c ;
} else {
return null ;
}
};
StringConverter<Double> converter = new StringConverter<Double>() {
@Override
public Double fromString(String s) {
if (s.isEmpty() || "-".equals(s) || ".".equals(s) || "-.".equals(s)) {
return 0.0 ;
} else {
return Double.valueOf(s);
}
}
@Override
public String toString(Double d) {
return d.toString();
}
};
TextFormatter<Double> textFormatter = new TextFormatter<>(converter, 0.0, filter);
TextField textField = new TextField();
textField.setTextFormatter(textFormatter);
You can make the regular expression more complex if needed, e.g. to support grouping characters ("1,000.0"
), localization ("1.003,14159"
if that is appropriate for the locale), or scientific-notation-like representations ("6.022E23"
, etc), and to enforce minimum or maximum values, etc. You can even do things such as modifying the change, so that if the user types a -
anywhere in the text, it just flips the sign of the number. (Refer to the TextFormatter.Change
documentation for that kind of functionality.)
Note you can get and set the double value (as provided by the converter) directly from the formatter, which has an ObjectProperty<Double> valueProperty()
. So you can do things like
// update text field:
double value = ... ;
textFormatter.setValue(value);
// listen for changes in double value represented in text field
// Listener will be invoked when the user commits an edit:
textFormatter.valueProperty().addListener((ObservableValue<? extends Double> obs, Double oldValue, Double newValue) -> {
System.out.println("User entered value: "+newValue.doubleValue());
});
Here is a SSCCE. The second text field is just there so that you can see the effect of moving focus to a different control (it "commits" the value and invokes the listener on the text formatter, if the value has changed; a similar thing happens if the user presses enter).
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import javafx.application.Application;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class NumericTextField extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
Pattern validEditingState = Pattern.compile("-?(([1-9][0-9]*)|0)?(\.[0-9]*)?");
UnaryOperator<TextFormatter.Change> filter = c -> {
String text = c.getControlNewText();
if (validEditingState.matcher(text).matches()) {
return c ;
} else {
return null ;
}
};
StringConverter<Double> converter = new StringConverter<Double>() {
@Override
public Double fromString(String s) {
if (s.isEmpty() || "-".equals(s) || ".".equals(s) || "-.".equals(s)) {
return 0.0 ;
} else {
return Double.valueOf(s);
}
}
@Override
public String toString(Double d) {
return d.toString();
}
};
TextFormatter<Double> textFormatter = new TextFormatter<>(converter, 0.0, filter);
TextField textField = new TextField();
textField.setTextFormatter(textFormatter);
textFormatter.valueProperty().addListener((ObservableValue<? extends Double> obs, Double oldValue, Double newValue) -> {
System.out.println("User entered value: "+newValue.doubleValue());
});
VBox root = new VBox(5, textField, new TextField());
root.setAlignment(Pos.CENTER);
primaryStage.setScene(new Scene(root, 250, 250));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}