How to implement a simple ValidatingTextField in JavaFX

Diesmal möchte ich euch zeigen, wie ihr euch recht schnell ein ValidatingTextField erstellen könnt.

Im Grunde handelt es sich dabei, um eine Klasse, die von TextField erbt, und einen Listener sowie einige Properties, die die Klasse erweiterten. Wann ein ValidatingTextField als valide gelten soll, könnt ihr selbst mit Hilfe eines Regex definieren. Außerdem habt ihr die Möglichkeit, dem Nutzer direkt sichtbar zu machen, ob es sich um ein Pflichtfeld handelt oder nicht.

Implementierung

ValidatingTextField

package de.codelife;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.TextField;

/**
 * Eine Erweitung des normalen {@link TextField} aus JavaFx.<br>
 * <p>
 * Es ermöglicht ungewollte Eingaben mit Hilfe einer Regex zu beschränken.<br>
 * Dem Nutzer wird durch automatische Änderung der Hintergrundfarbe eine direkte RÜckmeldung gegeben, ob seine Eingabe für dieses Feld valide ist oder nicht.
 * <br>
 * � codelife.de
 */
public class ValidatingTextField extends TextField {

    private final ReadOnlyObjectWrapper<Boolean> validProperty = new ReadOnlyObjectWrapper<>(false);
    private final SimpleObjectProperty<String> regexProperty = new SimpleObjectProperty<>("");
    private final SimpleObjectProperty<Boolean> mandatoryProperty = new SimpleObjectProperty<>(false);

    /**
     * Diese Methode wird von JavaFX/SceneBuilder benötigt.<br>
     * Alternativ gibt es noch folgende Konstruktoren:
     * <p>
     * {@link #ValidatingTextField(java.lang.String, boolean)}<br>
     * {@link #ValidatingTextField(java.lang.String, java.lang.String, boolean) }
     * </p>
     * <p>
     * Diese können benutzt werden, wenn man ein {@link ValidatingTextField} ohne FXML erzeugen möchte.<br>
     * </p>
     *
     */
    public ValidatingTextField(){
        this("", false);
    }

    /**
     * @param regex Regex für die Validierung
     * @param mandatory Ob es sich um ein Pflichfeld handelt
     */
    public ValidatingTextField(String regex, boolean mandatory){
        this("", regex, mandatory);
    }

    /**
     * @param text Text der in das TextField direkt eingetragen werden soll
     * @param regex Regex für die Validierung
     * @param mandatory Ob es sich um ein Pflichfeld handelt
     */
    public ValidatingTextField(String text, String regex, boolean mandatory){
        super(text);
        this.getStylesheets().add("de/codelife/validatingTextField.css");
        this.mandatoryProperty.addListener(this::onMandatoryPropertyChanged);
        this.textProperty().addListener(this::onTextPropertyChanged);
        this.regexProperty.setValue(regex);
        this.mandatoryProperty.setValue(mandatory);
    }

    private void onMandatoryPropertyChanged(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue){
        if(newValue){
            if(!this.getStyleClass().contains("mandatory")){
                this.getStyleClass().add("mandatory");
            }
        } else {
            if(this.getStyleClass().contains("mandatory")){
                this.getStyleClass().remove("mandatory");
            }
        }
    }

    private void onTextPropertyChanged(ObservableValue<? extends String> observable, String oldValue, String newValue){
        if(newValue != null){
            validProperty.setValue(newValue.matches(regexProperty.getValue()));
            if(!newValue.isEmpty()) {
                changeStyle();
            } else {
                this.getStyleClass().removeAll("is_valid","is_invalid");
            }
        }
    }

    private void changeStyle(){
        if(validProperty.getValue()){
            if(!this.getStyleClass().contains("is_valid")){
                this.getStyleClass().remove("is_invalid");
                this.getStyleClass().add("is_valid");
            }
        } else {
            if(!this.getStyleClass().contains("is_invalid")){
                this.getStyleClass().remove("is_valid");
                this.getStyleClass().add("is_invalid");
            }
        }
    }

    /**
     * @return Ob die Eingaben valide sind
     */
    public final ReadOnlyObjectProperty<Boolean> validProperty(){
        return validProperty.getReadOnlyProperty();
    }

    /**
     * @return Die RegexProperty für dieses {@link ValidatingTextField}
     */
    public final ObjectProperty<String> regexProperty(){
        return regexProperty;
    }

    /**
     * @return Die Regex als String
     */
    public String getRegex(){
        return regexProperty.getValue();
    }

    /**
     * @param value Die Regex als String
     */
    public void setRegex(String value){
        regexProperty.setValue(value);
    }

    /**
     * @return Ob das {@link ValidatingTextField} ein Pflichtfeld ist.
     */
    public final ObjectProperty<Boolean> mandatoryProperty(){
        return mandatoryProperty;
    }

    /**
     * @return Ob das {@link ValidatingTextField} ein Pflichtfeld ist.
     */
    public boolean isMandatory(){
        return mandatoryProperty.getValue();
    }

    /**
     * @param value Ob das {@link ValidatingTextField} ein Pflichtfeld sein soll.
     */
    public void setMandatory(boolean value){
        mandatoryProperty.setValue(value);
    }
}

validatingTextField.css

.mandatory {
    -fx-text-fill: -fx-text-inner-color;
    -fx-highlight-fill: derive(#FFE5E5, -20%);
    -fx-highlight-text-fill: -fx-text-inner-color;
    -fx-prompt-text-fill: derive(#FFE5E5, -30%);
    -fx-background-color:
            #FF0728,
            -fx-control-inner-background,
            #FF072822,
            linear-gradient(from 0px 0px to 0px 5px, derive(#FFE5E5, -9%), #FFE5E5);
    -fx-background-radius: 3,2,4,0;
}

.is_valid {
    -fx-text-fill: -fx-text-inner-color;
    -fx-highlight-fill: derive(#D7FFC6, -20%);
    -fx-highlight-text-fill: -fx-text-inner-color;
    -fx-prompt-text-fill: derive(#D7FFC6, -30%);
    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%) ,-fx-text-box-border),
        linear-gradient(from 0px 0px to 0px 5px, derive(#D7FFC6, -9%), #D7FFC6);
    -fx-background-insets: -0.2,1,-1.4,3;
    -fx-background-radius: 3,2,4,0;
}

.is_valid:focused {
    -fx-text-fill: -fx-text-inner-color;
    -fx-highlight-fill: -fx-accent;
    -fx-highlight-text-fill: -fx-text-inner-color;
    -fx-prompt-text-fill: derive(#D7FFC6, -30%);
     -fx-background-color:
            #27C400,
            -fx-control-inner-background,
            #27C40022,
            linear-gradient(from 0px 0px to 0px 5px, derive(#D7FFC6, -9%), #D7FFC6);
    -fx-background-insets: -0.2,1,-1.4,3;
    -fx-background-radius: 3,2,4,0;
}

.is_invalid {
    -fx-text-fill: -fx-text-inner-color;
    -fx-highlight-fill: derive(#FFE5E5, -20%);
    -fx-highlight-text-fill: -fx-text-inner-color;
    -fx-prompt-text-fill: derive(#FFE5E5, -30%);
    -fx-background-color: linear-gradient(to bottom, derive(-fx-text-box-border, -10%) ,-fx-text-box-border),
        linear-gradient(from 0px 0px to 0px 5px, derive(#FFE5E5, -9%), #FFE5E5);
    -fx-background-insets: -0.2,1,-1.4,3;
    -fx-background-radius: 3,2,4,0;
}
.is_invalid:focused {
    -fx-text-fill: -fx-text-inner-color;
    -fx-highlight-fill: -fx-accent;
    -fx-highlight-text-fill: -fx-text-inner-color;
    -fx-prompt-text-fill: derive(#FFE5E5, -30%);
    -fx-background-color:
        #FF0728,
        -fx-control-inner-background,
        #FF072822,
        linear-gradient(from 0px 0px to 0px 5px, derive(#FFE5E5, -9%), #FFE5E5);
    -fx-background-insets: -0.2,1,-1.4,3;
    -fx-background-radius: 3,2,4,0;
}

Test

Mit Hilfe von TestFX habe ich das ValidatingTextField auf die korrekte Änderung der StyleClasses und der Properties getestet. Im folgenden könnt ihr einen  kleinen Ausschnitt der Testklasse sehen. Die komplette Klasse findet ihr in Bitbucket.

Um TestFX in eurem Projekt verwenden zu können, muss die pom.xml noch wie folgt angepasst werden.

pom

 <dependencies>
        <dependency>
            <groupId>org.testfx</groupId>
            <artifactId>testfx-junit</artifactId>
            <version>4.0.4-alpha</version>
        </dependency>
        <dependency>
            <groupId>org.testfx</groupId>
            <artifactId>testfx-core</artifactId>
            <version>4.0.4-alpha</version>
        </dependency>
    </dependencies>

ValidatingTextFieldTest

public class ValidatingTextFieldTest extends ApplicationTest {

    private static final String REGEX = "[0-9]+";

    @Override
    public void start(Stage stage) throws Exception {
        ValidatingTextField notMandatoryValidationTextField = new ValidatingTextField(REGEX, false);
        notMandatoryValidationTextField.setId("notMandatoryValidationTextField");
        ValidatingTextField mandatoryValidationTextField = new ValidatingTextField(REGEX, true);
        mandatoryValidationTextField.setId("mandatoryValidationTextField");
        ...
        stage.show();
    }

    @Test
    public void validPropertyShouldBeTrueTest(){
        ValidatingTextField notMandatoryValidationTextField = findNotMandatoryValidationTextField();
        ValidatingTextField mandatoryValidationTextField = findMandatoryValidationTextField();
        clickOn(notMandatoryValidationTextField).write("0987654321");
        clickOn(mandatoryValidationTextField).write("0987654321");
        WaitForAsyncUtils.waitForFxEvents();
        verifyThat(notMandatoryValidationTextField.validProperty().getValue(), is(true));
        verifyThat(mandatoryValidationTextField.validProperty().getValue(), is(true));
    }

    @Test
    public void validPropertyShouldBeFalseTest(){
        ValidatingTextField notMandatoryValidationTextField = findNotMandatoryValidationTextField();
        ValidatingTextField mandatoryValidationTextField = findMandatoryValidationTextField();
        clickOn(notMandatoryValidationTextField).write("asdf");
        clickOn(mandatoryValidationTextField).write("asdf");
        WaitForAsyncUtils.waitForFxEvents();
        verifyThat(notMandatoryValidationTextField.validProperty().getValue(), is(false));
        verifyThat(mandatoryValidationTextField.validProperty().getValue(), is(false));
    }
...

Scene Builder

Das erstellte ValidatingTextField kann auch ganz einfach in den SceneBuilder integriert werde, um es wie eine normale Komponente benutzten zu können.  Dazu habe ich in dem Unterordner resources im Bitbucket eine fertige .jar-Datei abgelegt, welche über die Oberfläche des SceneBuilders eingebunden werden kann (siehe Bilder).

SceneBuilder_Import_1

SceneBuilder_Import_2

SceneBuilder_Import_ValidatingTextField

Link

Das komplette Projekt findet ihr in Bitbucket.

 

How to implement a simple ValidatingTextField in JavaFX
Markiert in:                 

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.