Numeric TextField for Integers in JavaFX 8 with TextFormatter and/or UnaryOperator
Asked Answered
A

4

19

I am trying to create a numeric TextField for Integers by using the TextFormatter of JavaFX 8.

Solution with UnaryOperator:

UnaryOperator<Change> integerFilter = change -> {
    String input = change.getText();
    if (input.matches("[0-9]*")) { 
        return change;
    }
    return null;
};

myNumericField.setTextFormatter(new TextFormatter<String>(integerFilter));

Solution with IntegerStringConverter:

myNumericField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter()));  

Both solutions have their own problems. With the UnaryOperator, I can only enter digits from 0 to 9 like intended, but I also need to enter negative values like "-512", where the sign is only allowed at the first position. Also I don't want numbers like "00016" which is still possible.

The IntegerStringConverter method works way better: Every invalid number like "-16-123" is not accepted and numbers like "0123" get converted to "123". But the conversion only happens when the text is commited (via pressing enter) or when the TextField loses its focus.

Is there a way to enforce the conversion of the second method with the IntegerStringConverter every time the value of the TextField is updated?

Aquatint answered 7/11, 2016 at 19:5 Comment(0)
P
34

The converter is different to the filter: the converter specifies how to convert the text to a value, and the filter filters changes the user may make. It sounds like here you want both, but you want the filter to more accurately filter the changes that are allowed.

I usually find it easiest to check the new value of the text if the change were accepted. You want to optionally have a -, followed by 1-9 with any number of digits after it. It's important to allow an empty string, else the user won't be able to delete everything.

So you probably need something like

UnaryOperator<Change> integerFilter = change -> {
    String newText = change.getControlNewText();
    if (newText.matches("-?([1-9][0-9]*)?")) { 
        return change;
    }
    return null;
};

myNumericField.setTextFormatter(
    new TextFormatter<Integer>(new IntegerStringConverter(), 0, integerFilter));

You can even add more functionality to the filter to let it process - in a smarter way, e.g.

UnaryOperator<Change> integerFilter = change -> {
    String newText = change.getControlNewText();
    // if proposed change results in a valid value, return change as-is:
    if (newText.matches("-?([1-9][0-9]*)?")) { 
        return change;
    } else if ("-".equals(change.getText()) ) {

        // if user types or pastes a "-" in middle of current text,
        // toggle sign of value:

        if (change.getControlText().startsWith("-")) {
            // if we currently start with a "-", remove first character:
            change.setText("");
            change.setRange(0, 1);
            // since we're deleting a character instead of adding one,
            // the caret position needs to move back one, instead of 
            // moving forward one, so we modify the proposed change to
            // move the caret two places earlier than the proposed change:
            change.setCaretPosition(change.getCaretPosition()-2);
            change.setAnchor(change.getAnchor()-2);
        } else {
            // otherwise just insert at the beginning of the text:
            change.setRange(0, 0);
        }
        return change ;
    }
    // invalid change, veto it by returning null:
    return null;
};

This will let the user press - at any point and it will toggle the sign of the integer.

SSCCE:

import java.util.function.UnaryOperator;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.IntegerStringConverter;

public class IntegerFieldExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TextField integerField = new TextField();
        UnaryOperator<Change> integerFilter = change -> {
            String newText = change.getControlNewText();
            if (newText.matches("-?([1-9][0-9]*)?")) { 
                return change;
            } else if ("-".equals(change.getText()) ) {
                if (change.getControlText().startsWith("-")) {
                    change.setText("");
                    change.setRange(0, 1);
                    change.setCaretPosition(change.getCaretPosition()-2);
                    change.setAnchor(change.getAnchor()-2);
                    return change ;
                } else {
                    change.setRange(0, 0);
                    return change ;
                }
            }
            return null;
        };
        
        // modified version of standard converter that evaluates an empty string 
        // as zero instead of null:
        StringConverter<Integer> converter = new IntegerStringConverter() {
            @Override
            public Integer fromString(String s) {
                if (s.isEmpty()) return 0 ;
                return super.fromString(s);
            }
        };

        TextFormatter<Integer> textFormatter = 
                new TextFormatter<Integer>(converter, 0, integerFilter);
        integerField.setTextFormatter(textFormatter);
        
        // demo listener:
        textFormatter.valueProperty().addListener((obs, oldValue, newValue) -> System.out.println(newValue));
        
        VBox root = new VBox(5, integerField, new Button("Click Me"));
        root.setAlignment(Pos.CENTER);
        Scene scene = new Scene(root, 300, 120);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Tutorial

A comprehensive tutorial guide on using a TextFormatter:

Prehistory answered 7/11, 2016 at 19:14 Comment(2)
Smart use of ? on the number group! Was thinking about -?([1-9]?|[1-9][0-9]*), but this is much cleaner!Flea
I really appreciate your effort, your solution worked perfectly!Aquatint
A
3

Is there a way to enforce the conversion of the second method with the IntegerStringConverter every time the value of the TextField is updated?

Although the answer by @James_D gives you what you want, I would like to add a different perspective also. Your idea is to hold the hand of the user, with every single keypress. This can be helpful, but it can also be frustrating to the user. Actions like copy pasting into the text field, or editing an existing input at different positions, do not work well with the hand-holding approach.

A reason why you might want to apply the conversion/filtering immediately is because the user may not be aware of the input being invalid, and perhaps missing the correction when tabbing to the next field. So how about instead of restricting what the user can input, you visualize whether the current input is valid or not, without changing the text contents. So for instance, you could add a red border to the text field while the contents are invalid. You could still use a StringConverter in addition to this.

For example

myNumericField.setTextFormatter(new TextFormatter<>(new IntegerStringConverter()));
myNumericField.textProperty().addListener((obs,oldv,newv) -> {
    try {
        myNumericField.getTextFormatter().getValueConverter().fromString(newv);
        // no exception above means valid
        myNumericField.setBorder(null);
    } catch (NumberFormatException e) {
        myNumericField.setBorder(new Border(new BorderStroke(Color.RED, BorderStrokeStyle.SOLID, new CornerRadii(3), new BorderWidths(2), new Insets(-2))));
    }
});

text field with red border

The converter can also easily be extended to limit the valid number range.

Apparent answered 6/8, 2021 at 16:24 Comment(1)
imo this is a very elegant solution: no regex, clean separation between formatting, conversion and validation (the latter not being shown in this example but easy to extend). Thanks for this!Miles
O
0
 TextField txtpoint = new TextField();
    txtpoint.textProperty().addListener(new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
            if (!newValue.isEmpty()) {
                try {
                    long pointI = Integer.parseInt(newValue);
                    txtpoint.setText(String.valueOf(pointI));
                } catch (Exception e) {
                    txtpoint.clear();
                    txtpoint.setText(getNumber(oldValue));
                }
            }
        }
    });


private String getNumber(String value) {
    String n = "";
    try {
        return String.valueOf(Integer.parseInt(value));
    } catch (Exception e) {
        String[] array = value.split("");
        for (String tab : array) {
            try {
                System.out.println(tab);
                n = n.concat(String.valueOf(Integer.parseInt(String.valueOf(tab))));
            } catch (Exception ex) {
                System.out.println("not nomber");
            }
        }
        return n;
    }
}
Osculation answered 22/8, 2017 at 7:2 Comment(2)
On stack overflow some words of explanation / usage instruction usually are welcomed.Bedivere
since fx8u80 this is wrong (you must not change the property you are listening to), use a textFormatter insteadSabir
H
0

Here is my solution :

    integerText.setTextFormatter(new TextFormatter<>(new IntegerStringConverter(), 0, new UnaryOperator<TextFormatter.Change>() {
        @Override
        public TextFormatter.Change apply(TextFormatter.Change change) {
            NumberFormat numberFormat = NumberFormat.getIntegerInstance();
            ParsePosition position = new ParsePosition(0);
            Object obj = numberFormat.parseObject(change.getControlNewText(),position);
            if(obj != null && position.getIndex() == change.getControlNewText().length()){
                return change;
            }
            return null;
        }
    }));
Habit answered 14/7, 2022 at 14:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.