JavaFX中制作数字文本框的推荐方法是什么?

106

我需要限制一个TextField输入整数,有什么建议吗?


4
注意:监听textProperty是__错误的__!在拥有TextFormatter之前(自fx8u40左右起),这是最好的方法。从那时起,基本规则适用:一般认为在此方法中修改观察到的值是不良实践,使用TextFormatter是__唯一可接受的解决方案__。 - kleopatra
24个回答

147

虽然这个帖子非常古老,但是这种方法看起来更整洁,并且可以去除复制过来的非数字字符。

// force the field to be numeric only
textField.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, 
        String newValue) {
        if (!newValue.matches("\\d*")) {
            textField.setText(newValue.replaceAll("[^\\d]", ""));
        }
    }
});

4
非常好用。请注意,如果您想节省几个字符,还可以使用 \\D+(或只是 \\D)代替 [^\\d] - ricky3350
5
更简单的方法是将文本设置回旧值。这样你就不必纠结于正则表达式替换(很遗憾,对我来说它根本不起作用)。 - geisterfurz007
也许这是另一个问题的主题,但如果我想强制输入double,那么我应该写什么而不是\\d*[^\\d]?@EvanKnowles - Robb1
13
已过时,请参见下面的TextFormatter答案。 - gerardw
4
可以尝试使用 Integer.parseInt(newValue),并使用 trycatch 来捕获 NumberFormatException 错误。 - user1300214
显示剩余3条评论

57

javafx.scene.control.TextFormatter

更新于2016年4月

这篇答案是几年前创建的,原始答案现在基本已经过时。

自Java 8u40以来,Java有一个TextFormatter,通常最适合强制执行特定格式的输入,例如在JavaFX TextFields上的数字:

还可以查看此问题的其他答案,其中明确提到了TextFormatter。


原始答案

在这个代码片段中有一些示例,我在下面复制了一个示例:

// helper text field subclass which restricts text input to a given range of natural int numbers
// and exposes the current numeric int value of the edit box as a value property.
class IntField extends TextField {
  final private IntegerProperty value;
  final private int minValue;
  final private int maxValue;
 
  // expose an integer value property for the text field.
  public int  getValue()                 { return value.getValue(); }
  public void setValue(int newValue)     { value.setValue(newValue); }
  public IntegerProperty valueProperty() { return value; }
    
  IntField(int minValue, int maxValue, int initialValue) {
    if (minValue > maxValue) 
      throw new IllegalArgumentException(
        "IntField min value " + minValue + " greater than max value " + maxValue
      );
    if (!((minValue <= initialValue) && (initialValue <= maxValue))) 
      throw new IllegalArgumentException(
        "IntField initialValue " + initialValue + " not between " + minValue + " and " + maxValue
      );
 
    // initialize the field values.
    this.minValue = minValue;
    this.maxValue = maxValue;
    value = new SimpleIntegerProperty(initialValue);
    setText(initialValue + "");
 
    final IntField intField = this;
 
    // make sure the value property is clamped to the required range
    // and update the field's text to be in sync with the value.
    value.addListener(new ChangeListener<Number>() {
      @Override public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
        if (newValue == null) {
          intField.setText("");
        } else {
          if (newValue.intValue() < intField.minValue) {
            value.setValue(intField.minValue);
            return;
          }
 
          if (newValue.intValue() > intField.maxValue) {
            value.setValue(intField.maxValue);
            return;
          }
 
          if (newValue.intValue() == 0 && (textProperty().get() == null || "".equals(textProperty().get()))) {
            // no action required, text property is already blank, we don't need to set it to 0.
          } else {
            intField.setText(newValue.toString());
          }
        }
      }
    });
 
    // restrict key input to numerals.
    this.addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
      @Override public void handle(KeyEvent keyEvent) {
        if(intField.minValue<0) {
                if (!"-0123456789".contains(keyEvent.getCharacter())) {
                    keyEvent.consume();
                }
            }
            else {
                if (!"0123456789".contains(keyEvent.getCharacter())) {
                    keyEvent.consume();
                }
            }
      }
    });
      
    // ensure any entered values lie inside the required range.
    this.textProperty().addListener(new ChangeListener<String>() {
      @Override public void changed(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {
        if (newValue == null || "".equals(newValue) || (intField.minValue<0 && "-".equals(newValue))) {
          value.setValue(0);
          return;
        }
 
        final int intValue = Integer.parseInt(newValue);
 
        if (intField.minValue > intValue || intValue > intField.maxValue) {
          textProperty().setValue(oldValue);
        }
          
        value.set(Integer.parseInt(textProperty().get()));
      }
    });
  }
}

“minValue > maxValue” 这句话不是等同于 “maxValue < minValue” 吗?同时,“IntField max value ”+ minValue +“ less than min value ”+ maxValue 应该改为“IntField max value ”+ maxValue +“ less than min value ”+ minValue。 - motaa
1
是的,我编辑了代码以删除具有错误消息的冗余语句。 - jewelsea

44

我知道这是一个相当古老的帖子,但是为了未来的读者,这里有另一个我认为非常直观的解决方案:

public class NumberTextField extends TextField
{

    @Override
    public void replaceText(int start, int end, String text)
    {
        if (validate(text))
        {
            super.replaceText(start, end, text);
        }
    }

    @Override
    public void replaceSelection(String text)
    {
        if (validate(text))
        {
            super.replaceSelection(text);
        }
    }

    private boolean validate(String text)
    {
        return text.matches("[0-9]*");
    }
}

编辑:感谢none_SCBoy提出的建议改进。


1
有人知道用于十进制数(例如123.456)的正则表达式是什么吗?validate(text)函数参数"text"的值是最后一个被用户编辑的字符,而不是文本字段中的整个字符串,因此正则表达式可能无法正确匹配。例如,我使用以下命令:text.matches("\\d+");但我无法删除文本字段中的任何字符。 - mils
1
@mils 看看这里:链接1链接2 - vellotis
1
我认为这是一个好的解决方案,但我会改变一件事。不要写text.matches("[0-9]*"),而是创建一个模式变量并编译它。在你的代码中,每次有人在字段中写入字符时都会编译模式,这对CPU和内存来说是昂贵的。所以我会添加变量:private static Pattern integerPattern = Pattern.compile("[0-9]*");并用以下内容替换验证主体:integerPattern.matcher(text).matches(); - P. Jowko

42

从JavaFX 8u40开始,您可以在文本字段上设置TextFormatter对象:

UnaryOperator<Change> filter = change -> {
    String text = change.getText();

    if (text.matches("[0-9]*")) {
        return change;
    }

    return null;
};
TextFormatter<String> textFormatter = new TextFormatter<>(filter);
fieldNport = new TextField();
fieldNport.setTextFormatter(textFormatter);

这样做既避免了子类化,也避免了重复的更改事件。如果您向文本属性添加更改侦听器并在该侦听器中修改文本,则会出现这些问题。


1
这很糟糕。如果您像上面那样使用getText,就无法将控制字符输入到TextField中,例如退格、箭头等。请改用以下方式:control.setTextFormatter(new TextFormatter <> (change -> change.getControlNewText().matches(regex) ? change : null)); - guymac

33

TextInput有一个TextFormatter,可用于格式化、转换和限制输入文本的类型。

TextFormatter具有过滤器,可用于拒绝输入。我们需要将其设置为拒绝任何无效的整数。它还具有转换器,我们需要将其设置为将字符串值转换为稍后可以绑定的整数值。

让我们创建一个可重复使用的过滤器:

public class IntegerFilter implements UnaryOperator<TextFormatter.Change> {
    private final static Pattern DIGIT_PATTERN = Pattern.compile("\\d*");

    @Override
    public Change apply(TextFormatter.Change aT) {
        return DIGIT_PATTERN.matcher(aT.getText()).matches() ? aT : null;
    }
}

过滤器可以做三件事情之一,它可以无修改地返回更改以接受它,它可以以某种方式改变更改并认为它合适,或者它可以返回null,以完全拒绝更改。

我们将使用标准的IntegerStringConverter作为转换器。

将所有东西放在一起:

TextField textField = ...;

TextFormatter<Integer> formatter = new TextFormatter<>(
    new IntegerStringConverter(), // Standard converter form JavaFX
    defaultValue, 
    new IntegerFilter());
formatter.valueProperty().bindBidirectional(myIntegerProperty);

textField.setTextFormatter(formatter);

如果你不需要一个可重复使用的过滤器,你可以用这个花式一行代码替代它:

TextFormatter<Integer> formatter = new TextFormatter<>(
    new IntegerStringConverter(), 
    defaultValue,  
    c -> Pattern.matches("\\d*", c.getText()) ? c : null );

21

我不喜欢异常,因此我使用了String类中的matches函数。

text.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, 
        String newValue) {
        if (newValue.matches("\\d*")) {
            int value = Integer.parseInt(newValue);
        } else {
            text.setText(oldValue);
        }
    }
});

1
有时候你会遇到一个异常,因为数字可能太大了。 - schlagi123
1
提示:将正则表达式"\d+"替换为"\d*"。这样可以让你从文本输入框中删除所有字符(使用退格/删除键清除字段)。 - Guus
1
不要忘记在将光标位置设置回旧值后重新设置它的位置:textField.positionCaret(textField.getLength()); - hsson
如果你想在这种情况下限制输入为4位数字,那么请将第一个if条件改为:if (newValue.matches("\\d*") && newValue.getText().length < 5) - Quatsch
@Cappuccino90 你应该交换语句的顺序,因为在每次命中时检查长度比解析正则表达式要便宜得多。 - thatsIch

13

Java SE 8u40开始,您可以使用一个"integer"Spinner来安全地通过键盘的上箭头/下箭头键或提供的按钮选择有效整数。

您还可以定义最小值最大值初始值以限制允许的值,并指定每步增加或减少的数量。

例如:

// Creates an integer spinner with 1 as min, 10 as max and 2 as initial value
Spinner<Integer> spinner1 = new Spinner<>(1, 10, 2);
// Creates an integer spinner with 0 as min, 100 as max and 10 as initial 
// value and 10 as amount to increment or decrement by, per step
Spinner<Integer> spinner2 = new Spinner<>(0, 100, 10, 10);

使用"整数"和"双精度浮点数"微调器的结果示例。

enter image description here

微调器是一种单行文本字段控件,允许用户从有序序列中选择数字或对象值。 微调器通常提供一对微小的箭头按钮,用于逐个步进顺序中的元素。 键盘的上箭头/下箭头键也可以循环遍历元素。 用户还可以直接输入(合法)值到微调器中。 尽管组合框提供类似的功能,但微调器有时更受青睐,因为它们不需要可能会遮挡重要数据的下拉列表,并且还允许功能,例如从最大值回到最小值进行包装(例如,从最大正整数到0)。

有关微调器控件的更多详细信息


1
人们不应该忽略Spinner在文本输入或失去焦点时无法验证的问题。 - geometrikal
如果旋转器包含一个文本格式化器来过滤非数字字符,那就太理想了,但是现在这样有点不方便。 - undefined

12

如果您使用Java 1.8 Lambda表达式,首选答案甚至可以更小。

textfield.textProperty().addListener((observable, oldValue, newValue) -> {
    if (newValue.matches("\\d*")) return;
    textfield.setText(newValue.replaceAll("[^\\d]", ""));
});

感谢我的朋友,这个答案不会在输出终端产生问题。 - Spider-Man
谢谢我的朋友,这个答案不会在输出终端上产生问题。 - Spider-Man

8

我想借助Evan Knowles的回答和JavaFX 8中的TextFormatter来帮助您实现我的想法。

textField.setTextFormatter(new TextFormatter<>(c -> {
    if (!c.getControlNewText().matches("\\d*")) 
        return null;
    else
        return c;
    }
));

祝你好运;保持冷静,编写Java代码。


6
TextField text = new TextField();

text.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable,
            String oldValue, String newValue) {
        try {
            Integer.parseInt(newValue);
            if (newValue.endsWith("f") || newValue.endsWith("d")) {
                manualPriceInput.setText(newValue.substring(0, newValue.length()-1));
            }
        } catch (ParseException e) {
            text.setText(oldValue);
        }
    }
});
< p>if< /p>子句对于处理像0.5d或0.7f这样的输入非常重要,这些输入通过Int.parseInt()正确解析,但不应出现在文本字段中。

9
啥?自从什么时候0.5d可以被Integer.parseInt()正确解析了?!它会抛出一个java.lang.NumberFormatException异常:"For input string: "0.5d"(预料之中,因为它不是一个整数) - Burkhard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接