我正在尝试创建一个TextField,其内容已通过模板验证。为此,我创建了一个TextFormatter,并向其中传递了StringConverter

但是,我确实注意到有关使用StringConverter<String>的怪异事情。当我输入无效数据并且该字段失去焦点时,它不会清除其内容(它只会在随后的聚焦之后清除它)。为了进行比较,当我使用StringConverter<LocalTime>时,未注意到此问题。

如果我抓住焦点的变化并验证数据,就可以解决问题,但是我想知道为什么两种情况下的验证都存在差异。

public class Sample extends Application {


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

    @Override
    public void start(Stage primaryStage) {
        TextField fieldA = new TextField();
        fieldA.setPromptText("00000");
        fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
            @Override
            public String toString(String object) {
                if(object == null) return "";
                return object.matches("[0-9]{5}") ? object : "";
            }

            @Override
            public String fromString(String string) {
                if(string == null) return null;
                return string.matches("[0-9]{5}") ? string : null;
            }
        }));

//        fieldA.focusedProperty().addListener((observable, oldValue, newValue) -> {
//            if(!fieldA.textProperty().getValueSafe().matches("[0-9]{5}")) {
//                fieldA.setText(null);
//            }
//        });

        TextField fieldB = new TextField();
        fieldB.setPromptText("HH:MM:SS");
        fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
            @Override
            public String toString(LocalTime object) {
                if(object == null) return "";
                return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
            }

            @Override
            public LocalTime fromString(String string) {
                if(string == null) return null;
                return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
            }
        }));

        VBox vBox = new VBox(fieldA, fieldB);
        vBox.setSpacing(5);

        primaryStage.setScene(new Scene(vBox));
        primaryStage.show();
    }
}


ps:请注意,其目的不是创建只能接受5个数字的TextField。这只是一个例子。

最佳答案

我发现了行为差异的原因。主要问题是通过将valueProperty(在TextFormatter中)与textProperty(在TextField中)绑定来完成控件的更新。因为仅在更改包装器的值时,对所有Property对象的更改通知才饱和,所以顺序的null提交会导致一次性通知。

使用StringConverter<LocalTime>时的不同行为是因为LocalTime::parse()在无效格式中引发了DateTimeParseException异常。这进而导致设置了新的valueProperty值以及先前的有效控制值。

这是负责此行为的TextFormatter的特定代码段。

void updateValue(String text) {
    if (!value.isBound()) {
        try {
            V v = valueConverter.fromString(text);
            setValue(v);
        } catch (Exception e) {
            updateText(); // Set the text with the latest value
        }
    }
}


问题的解决方案是使用无效值实现StringConverter::fromString,而不是返回null,应该抛出未经检查的异常。

public class Sample extends Application {


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

    @Override
    public void start(Stage primaryStage) {
        TextField fieldA = new TextField();
        fieldA.setPromptText("00000");
        fieldA.setTextFormatter(new TextFormatter<>(new StringConverter<String>() {
            @Override
            public String toString(String object) {
                if(object == null) return "";
                return object.matches("[0-9]{5}") ? object : "";
            }

            @Override
            public String fromString(String string) {
                if(string == null)
                    throw new RuntimeException("Value is null");

                if(string.matches("[0-9]{5}")) {
                    return string;
                }

                throw new RuntimeException("Value not match");
            }
        }));


        TextField fieldB = new TextField();
        fieldB.setPromptText("HH:MM:SS");
        fieldB.setTextFormatter(new TextFormatter<>(new StringConverter<LocalTime>() {
            @Override
            public String toString(LocalTime object) {
                if(object == null) return "";
                return object.format(DateTimeFormatter.ofPattern("HH:mm:ss"));
            }

            @Override
            public LocalTime fromString(String string) {
                if(string == null) return null;
                return LocalTime.parse(string, DateTimeFormatter.ofPattern("HH:mm:ss"));
            }
        }));


        VBox vBox = new VBox(fieldA, fieldB);
        vBox.setSpacing(5);

        primaryStage.setScene(new Scene(vBox));
        primaryStage.show();
    }
}

07-27 13:37