从JavaFX对话框中的数字文本字段转义

从JavaFX对话框中的数字文本字段转义,javafx,Javafx,我有一个带有几个UI元素的自定义对话框。有些文本字段是用于。当按escape键且焦点位于任何数字文本字段上时,此对话框不会关闭。当焦点位于没有此自定义TextFormatter的其他文本字段时,对话框将正常关闭 以下是简化代码: package application; import java.text.DecimalFormat; import java.text.ParsePosition; import javafx.application.Application; import ja

我有一个带有几个UI元素的自定义对话框。有些文本字段是用于。当按escape键且焦点位于任何数字文本字段上时,此对话框不会关闭。当焦点位于没有此自定义TextFormatter的其他文本字段时,对话框将正常关闭

以下是简化代码:

package application;

import java.text.DecimalFormat;
import java.text.ParsePosition;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            TextField name = new TextField();
            HBox hb1 = new HBox();
            hb1.getChildren().addAll(new Label("Name: "), name);

            TextField id = new TextField();
            id.setTextFormatter(getNumberFormatter()); // numbers only
            HBox hb2 = new HBox();
            hb2.getChildren().addAll(new Label("ID: "), id);

            VBox vbox = new VBox();
            vbox.getChildren().addAll(hb1, hb2);

            Dialog<ButtonType> dialog = new Dialog<>();
            dialog.setTitle("Number Escape");
            dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);
            dialog.getDialogPane().setContent(vbox);

            Platform.runLater(() -> name.requestFocus());

            if (dialog.showAndWait().get() == ButtonType.OK) {
                System.out.println("OK: " + name.getText() + id.getText());
            } else {
                System.out.println("Cancel");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    TextFormatter<Number> getNumberFormatter() {
        // from https://stackoverflow.com/a/31043122
        DecimalFormat format = new DecimalFormat("#");
        TextFormatter<Number> tf = 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;
            }
        });

        return tf;
    }

    public static void main(String[] args) {
        launch(args);
    }
}
包应用;
导入java.text.DecimalFormat;
导入java.text.ParsePosition;
导入javafx.application.application;
导入javafx.application.Platform;
导入javafx.scene.control.ButtonType;
导入javafx.scene.control.Dialog;
导入javafx.scene.control.Label;
导入javafx.scene.control.TextField;
导入javafx.scene.control.TextFormatter;
导入javafx.scene.layout.HBox;
导入javafx.scene.layout.VBox;
导入javafx.stage.stage;
公共类主扩展应用程序{
@凌驾
公共无效开始(阶段primaryStage){
试一试{
TextField name=新的TextField();
HBox hb1=新的HBox();
hb1.getChildren().addAll(新标签(“名称”)、名称);
TextField id=新的TextField();
id.setTextFormatter(getNumberFormatter());//仅限数字
HBox hb2=新的HBox();
hb2.getChildren().addAll(新标签(“ID”)、ID);
VBox VBox=新的VBox();
vbox.getChildren().addAll(hb1,hb2);
Dialog=新建Dialog();
对话框.setTitle(“数字转义”);
dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK,ButtonType.CANCEL);
dialog.getDialogPane().setContent(vbox);
Platform.runLater(()->name.requestFocus());
if(dialog.showAndWait().get()==ButtonType.OK){
System.out.println(“确定:+name.getText()+id.getText());
}否则{
系统输出打印项次(“取消”);
}
}捕获(例外e){
e、 printStackTrace();
}
}
TextFormatter getNumberFormatter(){
//从https://stackoverflow.com/a/31043122
DecimalFormat=新的DecimalFormat(“#”);
TextFormatter tf=新的TextFormatter(c->{
if(c.getControlNewText().isEmpty()){
返回c;
}
ParsePosition ParsePosition=新的ParsePosition(0);
Object Object=format.parse(c.getControlNewText(),parsePosition);
if(object==null | | parsePosition.getIndex()
当焦点在
id
上时,按下escape键时如何关闭对话框?

问题 在提供解决方案之前,我认为理解为什么使用
TextFormatter
似乎会改变
对话框的行为是重要的,或者至少是有趣的。如果这对你来说无关紧要,请跳到答案的末尾

取消按钮 根据
按钮
,取消按钮为:

如果场景中没有其他节点使用它,则接收键盘VK_ESC按下的按钮

那句话的结尾是重要的部分。取消按钮以及默认按钮的实现方式是通过注册
按钮所属的
场景
。仅当相应的
KeyEvent
冒泡进入
场景时,才会调用这些加速器。如果事件在到达
场景
之前被消耗,则不会调用加速器

注意:要了解JavaFX中事件处理的更多信息,特别是“气泡”和“消费”等术语,我建议阅读

对话
对话框
有关于如何以及何时关闭的特定规则。这些规则位于对话框关闭规则部分。只需说一句,基本上一切都取决于将哪个
按钮类型添加到
对话框窗格
。在您的示例中,您使用一种预定义类型:
ButtonType.CANCEL
。如果您查看该字段的名称,您将看到:

一种预定义的
ButtonType
,显示“取消”,并具有
ButtonBar.ButtonDa.Cancel的
ButtonBar.ButtonDa

如果您查看
按钮的数据。取消\u关闭
,您将看到:

“取消”或“关闭”按钮的标签

是取消按钮:

这意味着,至少对于默认实现而言,为所述
ButtonType.CANCEL创建的
按钮将是一个取消按钮。换句话说,
按钮
将其
cancelButton
属性设置为
true
。这就是通过按Esc键关闭
对话框的功能

注意:它是负责创建相应按钮的方法(可以为自定义覆盖)。虽然该方法的返回类型是
节点
,但如文档所述,返回
按钮
的实例是典型的

文本格式化程序 (核心)JavaFX中的每个控件都有三个组件:控件类、皮肤类和行为类。后一个类负责处理用户输入,例如鼠标和按键事件。在这种情况下,我们关心
TextInputControlBehavior
TextFieldBehavior
;前者是后者的超类

注意:与皮肤类不同,皮肤类在JavaFX9中成为公共API,行为类在JavaFX12.0.2中仍然是私有API。下面描述的大部分内容都是实现细节

TextInputControlBehavior
类注册一个
EventHandler
,它对按下Esc键做出反应,调用
cancelEdit(KeyEvent
@Override
protected void cancelEdit(KeyEvent event) {
    TextField textField = getNode();
    if (textField.getTextFormatter() != null) {
        textField.cancelEdit();
        event.consume();
    } else {
        super.cancelEdit(event);
    }
}
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
    if (event.getCode() == KeyCode.ESCAPE) {
        event.consume();
        dialog.close();
    }
});
public class TextFieldCancelSO extends Application {

    /**
     * Returns a boolean to indicate whether the given field has uncommitted
     * changes.
     * 
     * @param <T> the type of the formatter's value
     * @param field the field to analyse
     * @return true if the field has a textFormatter with converter and
     *    uncommitted changes, false otherwise
     */
    public static <T> boolean isDirty(TextField field) {
        TextFormatter<T> textFormatter = (TextFormatter<T>) field.getTextFormatter();
        if (textFormatter == null || textFormatter.getValueConverter() == null) return false;
        String fieldText = field.getText();
        StringConverter<T> valueConverter = textFormatter.getValueConverter();
        String formatterText = valueConverter.toString(textFormatter.getValue());
        // todo: handle empty string vs. null value
        return !Objects.equals(fieldText, formatterText);
    }

    /**
     * Install a custom keyMapping for ESCAPE in the inputMap of the given field. 
     * @param field the textField to configure
     */
    protected void installCancel(TextField field) {
        // Dirty: reflectively access the behavior
        // needs --add-exports at compile- and runtime! 
        // note: FXUtils is a custom helper class not contained in core fx, use your own 
        // helper or write the field access code as needed.
        TextFieldBehavior behavior = (TextFieldBehavior) FXUtils.invokeGetFieldValue(
                TextFieldSkin.class, field.getSkin(), "behavior");
        // Dirty: internal api/classes
        InputMap inputMap = behavior.getInputMap();
        KeyBinding binding = new KeyBinding(KeyCode.ESCAPE);
        // custom mapping that delegates to helper method
        KeyMapping keyMapping = new KeyMapping(binding, e ->  {
            cancelEdit(field, e);
        });
        // by default, mappings consume the event - configure not to
        keyMapping.setAutoConsume(false);
        // remove old
        inputMap.getMappings().remove(keyMapping);
        // add new
        inputMap.getMappings().add(keyMapping);
    }

    /**
     * Custom EventHandler that's mapped to ESCAPE.
     * 
     * @param field the field to handle a cancel for
     * @param ev the received keyEvent 
     */
    protected void cancelEdit(TextField field, KeyEvent ev) {
        boolean dirty = isDirty(field);
        field.cancelEdit();
        if (dirty) {
           ev.consume();
        }
    }

    private Parent createContent() {
        TextFormatter<String> fieldFormatter = new TextFormatter<>(
                TextFormatter.IDENTITY_STRING_CONVERTER, "textField ...");
        TextField field = new TextField();
        field.setTextFormatter(fieldFormatter);
        // listen to skin: behavior is available only after it's set
        field.skinProperty().addListener((src, ov, nv) -> {
            installCancel(field);
        });
        // just to see the state of the formatter
        Label fieldValue = new Label();
        fieldValue.textProperty().bind(fieldFormatter.valueProperty());

        // add cancel button
        Button cancel = new Button("I'm the cancel");
        cancel.setCancelButton(true);
        cancel.setOnAction(e -> LOG.info("triggered: " + cancel.getText()));

        HBox fields = new HBox(100, field, fieldValue);
        BorderPane content = new BorderPane(fields);
        content.setBottom(cancel);
        return content;
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.show();
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TextFieldCancelSO.class.getName());

}