Qual é a maneira recomendada de criar um TextField numérico em JavaFX?

85

Eu preciso restringir a entrada em um TextField para inteiros. Algum conselho?

Harry Mitchell
fonte
3
Nota: ouvir textProperty estão errados ! Isso foi o melhor que poderia ser feito antes de ter um TextFormatter (desde fx8u40 ou assim). Desde então, a regra básica se aplica: Em geral, é considerado uma má prática modificar o valor observado neste método e usar um TextFormatter é a única solução aceitável.
kleopatra

Respostas:

120

Tópico muito antigo, mas parece mais organizado e remove os caracteres não numéricos se colado.

// 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]", ""));
        }
    }
});
Evan Knowles
fonte
3
Funciona maravilhosamente. Observe que você também pode usar \\D+(ou apenas \\D) em vez de [^\\d], se quiser salvar alguns caracteres.
ricky3350
5
Ainda mais simples é definir o texto de volta para oldValue. Dessa forma, você não precisa mexer em substituições de regex (o que não funcionou para mim, infelizmente).
geisterfurz007
@ geisterfurz007 Isso dá a opção de remover não-numéricos do texto colado.
Evan Knowles
10
Desatualizado, consulte a resposta do TextFormatter abaixo.
gerardw
3
Também poderia simplesmente usar Integer.parseInt(newValue)e usar trye catchlocalizar um erroNumberFormatException
Pixel
44

Atualização de abril de 2016

Essa resposta foi criada há alguns anos e a resposta original está obsoleta agora.

Desde Java 8u40, Java tem um TextFormatter que geralmente é melhor para forçar a entrada de formatos específicos, como numéricos, em TextFields JavaFX:

Veja também outras respostas a esta pergunta que mencionam especificamente TextFormatter.


Resposta Original

Existem alguns exemplos disso nesta essência , eu dupliquei um dos exemplos abaixo:

// 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 (maxValue < minValue) 
      throw new IllegalArgumentException(
        "IntField max value " + minValue + " less than min 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()));
      }
    });
  }
}
jewelsea
fonte
43

Eu sei que este é um tópico bastante antigo, mas para futuros leitores, aqui está outra solução que achei bastante intuitiva:

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]*");
    }
}

Edit: Obrigado none_ e SCBoy por suas sugestões de melhorias.

Burkhard
fonte
1
Alguém sabe qual regex usar para números decimais (por exemplo, 123.456)? O valor do parâmetro de função validate (text) "text" é o último caractere editado pelo usuário, não a string inteira no campo de texto, portanto, a regex pode não corresponder corretamente. Por exemplo, estou usando este comando da seguinte maneira: text.matches("\\d+"); e não consigo excluir nenhum caractere no campo de texto
mils
1
@mils Olhe aqui e aqui .
vellotis
1
Acho que é uma boa solução, mas gostaria de mudar uma coisa. Em vez de escrever text.matches ("[0-9] *"), eu criaria uma variável para o padrão e compilaria. Em seu código, o padrão é compilado toda vez que alguém escreve um caractere em um campo, e isso é caro para CPU e memória. Então, eu adicionaria a variável: private static Pattern integerPattern = Pattern.compile ("[0-9] *"); E substitua validate body por: integerPattern.matcher (text) .matches ();
P. Jowko
38

A partir do JavaFX 8u40, você pode definir um objeto TextFormatter em um campo de texto:

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);

Isso evita a criação de subclasses e eventos de alteração duplicados que você obterá ao adicionar um ouvinte de alteração à propriedade de texto e modificar o texto nesse ouvinte.

Uwe
fonte
30

O TextInputtem um TextFormatterque pode ser usado para formatar, converter e limitar os tipos de texto que podem ser inseridos.

O TextFormatterpossui um filtro que pode ser usado para rejeitar entradas. Precisamos definir isso para rejeitar qualquer coisa que não seja um número inteiro válido. Ele também tem um conversor que precisamos definir para converter o valor da string em um valor inteiro que podemos ligar mais tarde.

Vamos criar um filtro reutilizável:

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;
    }
}

O filtro pode fazer uma das três coisas: pode retornar a alteração sem modificações para aceitá-la como está, pode alterar a alteração de alguma forma que considere adequada ou pode retornar nullpara rejeitar a alteração totalmente.

Usaremos o padrão IntegerStringConvertercomo conversor.

Juntando tudo, temos:

TextField textField = ...;

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

textField.setTextFormatter(formatter);

Se você quiser não precisa de um filtro reutilizável, pode fazer esta linha sofisticada:

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

Não gosto de exceções, por isso usei a matchesfunção String-Class

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);
        }
    }
});
thatsIch
fonte
1
às vezes você tem uma exceção, porque o nuber pode ser muito grande.
schlagi123
1
Dica: substitua o regex "\\ d +" por "\\ d *". Isso permitirá que você remova todos os caracteres do campo de entrada de texto (use backspace / delete para limpar o campo).
Guus
1
Não se esqueça de definir a posição circunflexo depois de defini-lo de volta para o valor antigo:textField.positionCaret(textField.getLength());
hsson
Altere a primeira condição se para: if (newValue.matches("\\d*") && newValue.getText().length < 5) se quiser limitar a entrada a 4 dígitos neste caso.
Cappuccino90
@ Cappuccino90 Você deve mudar as instruções porque a verificação de comprimento é muito mais barata em cada
ocorrência do
9

A partir do Java SE 8u40 , para tal necessidade você pode usar um " inteiro " Spinnerpermitindo selecionar com segurança um inteiro válido usando as teclas de seta para cima / seta para baixo do teclado ou os botões de seta para cima / seta para baixo fornecidos.

Você também pode definir um mínimo , um máximo e um valor inicial para limitar os valores permitidos e uma quantidade para aumentar ou diminuir, por etapa.

Por exemplo

// 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);

Exemplo de resultado com um spinner " inteiro " e um spinner " duplo "

insira a descrição da imagem aqui

Um controle giratório é um controle de campo de texto de linha única que permite ao usuário selecionar um número ou valor de objeto em uma sequência ordenada de tais valores. Spinners geralmente fornecem um par de pequenos botões de seta para percorrer os elementos da sequência. As teclas de seta para cima / seta para baixo do teclado também percorrem os elementos. O usuário também pode digitar um valor (legal) diretamente no botão giratório. Embora as caixas de combinação forneçam funcionalidade semelhante, os spinners às vezes são preferidos porque não requerem uma lista suspensa que pode obscurecer dados importantes e também porque permitem recursos como quebra automática do valor máximo de volta ao valor mínimo (por exemplo, do maior número inteiro positivo a 0).

Mais detalhes sobre o controle Spinner

Nicolas Filotto
fonte
As pessoas não deveriam: o spinner não valida na entrada de texto ou perdeu o foco
geometrikal
7

A resposta preferida pode ser ainda menor se você usar Java 1.8 Lambdas

textfield.textProperty().addListener((observable, oldValue, newValue) -> {
    if (newValue.matches("\\d*")) return;
    textfield.setText(newValue.replaceAll("[^\\d]", ""));
});
vesion
fonte
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);
        }
    }
});

A ifcláusula é importante para lidar com entradas como 0,5d ou 0,7f, que são analisadas corretamente por Int.parseInt (), mas não devem aparecer no campo de texto.

ubuntudroid
fonte
9
Woot? Desde quando 0.5d é analisado corretamente por Integer.parseInt () ?! Ele lança um java.lang.NumberFormatException: Para string de entrada: "0.5d" (como esperado, já que não é um inteiro)
Burkhard
4

Tente este código simples, ele fará o trabalho.

DecimalFormat format = new DecimalFormat( "#.0" );
TextField field = new TextField();
field.setTextFormatter( new TextFormatter<>(c ->
{
    if ( c.getControlNewText().isEmpty() )
    {
        return c;
    }

    ParsePosition parsePosition = new ParsePosition( 0 );
    Object object = format.parse( c.getControlNewText(), parsePosition );

    if ( object == null || parsePosition.getIndex() <          c.getControlNewText().length() )
    {
        return null;
    }
    else
    {
        return c;
    }
}));
Hassan Latif
fonte
3

Se você deseja aplicar o mesmo ouvinte a mais de um TextField, aqui está a solução mais simples:

TextField txtMinPrice, txtMaxPrice = new TextField();

ChangeListener<String> forceNumberListener = (observable, oldValue, newValue) -> {
    if (!newValue.matches("\\d*"))
      ((StringProperty) observable).set(oldValue);
};

txtMinPrice.textProperty().addListener(forceNumberListener);
txtMaxPrice.textProperty().addListener(forceNumberListener);
javasuns
fonte
3

Este funcionou para mim.

public void RestrictNumbersOnly(TextField tf){
    tf.textProperty().addListener(new ChangeListener<String>() {
        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, 
            String newValue) {
            if (!newValue.matches("|[-\\+]?|[-\\+]?\\d+\\.?|[-\\+]?\\d+\\.?\\d+")){
                tf.setText(oldValue);
            }
        }
    });
}
Martin
fonte
3

Quero ajudar com minha ideia combinando a resposta de Evan Knowles com o TextFormatterJavaFX 8

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

então boa sorte;) mantenha a calma e codifique java

Salah Eddine Chaibi
fonte
2

Aqui está uma classe simples que lida com algumas validações básicas em TextField, usando TextFormatterintroduzido no JavaFX 8u40

EDITAR:

(Código adicionado em relação ao comentário de Floern)

import java.text.DecimalFormatSymbols;
import java.util.regex.Pattern;

import javafx.beans.NamedArg;
import javafx.scene.control.TextFormatter;
import javafx.scene.control.TextFormatter.Change;

public class TextFieldValidator {

    private static final String CURRENCY_SYMBOL   = DecimalFormatSymbols.getInstance().getCurrencySymbol();
    private static final char   DECIMAL_SEPARATOR = DecimalFormatSymbols.getInstance().getDecimalSeparator();

    private final Pattern       INPUT_PATTERN;

    public TextFieldValidator(@NamedArg("modus") ValidationModus modus, @NamedArg("countOf") int countOf) {
        this(modus.createPattern(countOf));
    }

    public TextFieldValidator(@NamedArg("regex") String regex) {
        this(Pattern.compile(regex));
    }

    public TextFieldValidator(Pattern inputPattern) {
        INPUT_PATTERN = inputPattern;
    }

    public static TextFieldValidator maxFractionDigits(int countOf) {
        return new TextFieldValidator(maxFractionPattern(countOf));
    }

    public static TextFieldValidator maxIntegers(int countOf) {
        return new TextFieldValidator(maxIntegerPattern(countOf));
    }

    public static TextFieldValidator integersOnly() {
        return new TextFieldValidator(integersOnlyPattern());
    }

    public TextFormatter<Object> getFormatter() {
        return new TextFormatter<>(this::validateChange);
    }

    private Change validateChange(Change c) {
        if (validate(c.getControlNewText())) {
            return c;
        }
        return null;
    }

    public boolean validate(String input) {
        return INPUT_PATTERN.matcher(input).matches();
    }

    private static Pattern maxFractionPattern(int countOf) {
        return Pattern.compile("\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?");
    }

    private static Pattern maxCurrencyFractionPattern(int countOf) {
        return Pattern.compile("^\\" + CURRENCY_SYMBOL + "?\\s?\\d*(\\" + DECIMAL_SEPARATOR + "\\d{0," + countOf + "})?\\s?\\" +
                CURRENCY_SYMBOL + "?");
    }

    private static Pattern maxIntegerPattern(int countOf) {
        return Pattern.compile("\\d{0," + countOf + "}");
    }

    private static Pattern integersOnlyPattern() {
        return Pattern.compile("\\d*");
    }

    public enum ValidationModus {

        MAX_CURRENCY_FRACTION_DIGITS {
            @Override
            public Pattern createPattern(int countOf) {
                return maxCurrencyFractionPattern(countOf);
            }
        },

        MAX_FRACTION_DIGITS {
            @Override
            public Pattern createPattern(int countOf) {
                return maxFractionPattern(countOf);
            }
        },
        MAX_INTEGERS {
            @Override
            public Pattern createPattern(int countOf) {
                return maxIntegerPattern(countOf);
            }
        },

        INTEGERS_ONLY {
            @Override
            public Pattern createPattern(int countOf) {
                return integersOnlyPattern();
            }
        };

        public abstract Pattern createPattern(int countOf);
    }

}

Você pode usá-lo assim:

textField.setTextFormatter(new TextFieldValidator(ValidationModus.INTEGERS_ONLY).getFormatter());

ou você pode instanciá-lo em um arquivo fxml e aplicá-lo a um customTextField com as propriedades correspondentes.

app.fxml:

<fx:define>
    <TextFieldValidator fx:id="validator" modus="INTEGERS_ONLY"/>
</fx:define>

CustomTextField.class:

public class CustomTextField {

private TextField textField;

public CustomTextField(@NamedArg("validator") TextFieldValidator validator) {
        this();
        textField.setTextFormatter(validator.getFormatter());
    }
}

Código no github

jns
fonte
2

Isso é o que eu uso:

private TextField textField;
textField.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        if(!newValue.matches("[0-9]*")){
            textField.setText(oldValue);
        }

    }
});

O mesmo na notação lambda seria:

private TextField textField;
textField.textProperty().addListener((observable, oldValue, newValue) -> {
    if(!newValue.matches("[0-9]*")){
        textField.setText(oldValue);
    }
});
Aren Isa
fonte
2

Este método permite que TextField termine todo o processamento (copiar / colar / desfazer seguro). Não requer estender classes e permite que você decida o que fazer com o novo texto após cada mudança (para colocá-lo na lógica, ou voltar ao valor anterior, ou mesmo modificá-lo).

  // fired by every text property change
textField.textProperty().addListener(
  (observable, oldValue, newValue) -> {
    // Your validation rules, anything you like
      // (! note 1 !) make sure that empty string (newValue.equals("")) 
      //   or initial text is always valid
      //   to prevent inifinity cycle
    // do whatever you want with newValue

    // If newValue is not valid for your rules
    ((StringProperty)observable).setValue(oldValue);
      // (! note 2 !) do not bind textProperty (textProperty().bind(someProperty))
      //   to anything in your code.  TextProperty implementation
      //   of StringProperty in TextFieldControl
      //   will throw RuntimeException in this case on setValue(string) call.
      //   Or catch and handle this exception.

    // If you want to change something in text
      // When it is valid for you with some changes that can be automated.
      // For example change it to upper case
    ((StringProperty)observable).setValue(newValue.toUpperCase());
  }
);

Para o seu caso, basta adicionar essa lógica dentro. Funciona perfeitamente.

   if (newValue.equals("")) return; 
   try {
     Integer i = Integer.valueOf(newValue);
     // do what you want with this i
   } catch (Exception e) {
     ((StringProperty)observable).setValue(oldValue);
   }
gmatagmis
fonte
0

Mmmm. Eu tive esse problema semanas atrás. Como a API não fornece um controle para isso,
você pode usar o seu próprio.
Eu usei algo como:

public class IntegerBox extends TextBox {
    public-init var value : Integer = 0;
    protected function apply() {
        try {
            value = Integer.parseInt(text);
        } catch (e : NumberFormatException) {}
        text = "{value}";
    }
    override var focused = false on replace {apply()};
    override var action = function () {apply()}
}

É usado da mesma forma que um normal TextBox,
mas também tem um valueatributo que armazena o inteiro inserido.
Quando o controle perde o foco, ele valida o valor e o reverte (se não for válido).

Alba Mendez
fonte
2
Que língua é essa?
Stealth Rabino
É o JavaFX Script Rabbi, está obsoleto .
jewelsea
0

este código Faça seu textField Aceitar apenas o número

textField.lengthProperty().addListener((observable, oldValue, newValue) -> {
        if(newValue.intValue() > oldValue.intValue()){
            char c = textField.getText().charAt(oldValue.intValue());
            /** Check if the new character is the number or other's */
            if( c > '9' || c < '0'){
                /** if it's not number then just setText to previous one */
                textField.setText(textField.getText().substring(0,textField.getText().length()-1));
            }
        }
    });
Amine Harbaoui
fonte
0

Este código funciona bem para mim, mesmo se você tentar copiar / colar.

myTextField.textProperty().addListener((observable, oldValue, newValue) -> {
    if (!newValue.matches("\\d*")) {
        myTextField.setText(oldValue);

    }
});
Ahmed Jaouadi
fonte
-1

Em atualizações recentes do JavaFX, você deve definir um novo texto no método Platform.runLater assim:

    private void set_normal_number(TextField textField, String oldValue, String newValue) {
    try {
        int p = textField.getCaretPosition();
        if (!newValue.matches("\\d*")) {
            Platform.runLater(() -> {
                textField.setText(newValue.replaceAll("[^\\d]", ""));
                textField.positionCaret(p);
            });
        }
    } catch (Exception e) {
    }
}

É uma boa ideia definir a posição do cursor também.

bdshahab
fonte
1
Obrigado pela sua resposta! Você poderia explicar um pouco porque Platform.runLateré necessário?
TuringTux
@TuringTux na versão recente do JavaFX, ele lançará uma exceção se você não usar Platform.runLater
bdshahab
-1

Eu gostaria de melhorar a resposta de Evan Knowles: https://stackoverflow.com/a/30796829/2628125

No meu caso, tive aula com manipuladores para a parte do componente de interface do usuário. Inicialização:

this.dataText.textProperty().addListener((observable, oldValue, newValue) -> this.numericSanitization(observable, oldValue, newValue));

E o método numbericSanitization:

private synchronized void numericSanitization(ObservableValue<? extends String> observable, String oldValue, String newValue) {
    final String allowedPattern = "\\d*";

    if (!newValue.matches(allowedPattern)) {
        this.dataText.setText(oldValue);
    }
}

Palavra-chave - sincronizada palavra- é adicionada para evitar um possível problema de bloqueio de renderização em javafx se setText for chamado antes que o antigo termine a execução. É fácil de reproduzir se você começar a digitar os caracteres errados muito rápido.

Outra vantagem é que você mantém apenas um padrão para corresponder e apenas faz o rollback. É melhor porque você pode abstrair facilmente a solução para diferentes padrões de sanitização.

Oleksandr Knyga
fonte
-1
rate_text.textProperty().addListener(new ChangeListener<String>() {

    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        String s="";
        for(char c : newValue.toCharArray()){
            if(((int)c >= 48 && (int)c <= 57 || (int)c == 46)){
                s+=c;
            }
        }
        rate_text.setText(s);
    }
});

Isso funciona bem, pois permite que você insira apenas o valor inteiro e o valor decimal (com código ASCII 46).

ShubhamWanne
fonte