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

rub*_*bpa 15 javafx

我有一个带有几个UI元素的自定义对话框。一些TextField用于数字输入。当按下逃逸键并且焦点位于任何数字文本字段上时,此对话框不会关闭。当焦点位于没有此自定义TextFormatter的其他TextField上时,该对话框可以正常关闭。

这是简化的代码:

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 /sf/answers/2173018571/
        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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在焦点对准时按下逃生键时,如何关闭对话框id

Sla*_*law 8

问题

提供解决方案,我认为这很重要,或者至少有趣之前,要了解为什么TextFormatter似乎改变的行为Dialog。如果这对您来说无关紧要,请随时跳到答案的结尾。

取消按钮

根据该文件Button,取消按钮是:

如果场景中没有其他节点消耗它,则该按钮将接受键盘VK_ESC的按下。

该句子的结尾是重要的部分。的方式取消按钮,以及默认的按钮来实现时,是通过注册的加速器SceneButton所属。仅当适当的KeyEvent 气泡达到时,才调用这些加速器Scene。如果事件在到达之前就已消耗Scene,则不会调用加速器。

注意:要了解有关JavaFX中事件处理的更多信息,尤其是诸如“气泡”和“已消耗”之类的术语,建议阅读本教程

对话方块

A Dialog关于如何以及何时关闭它具有某些规则。这些规则记录在此处的“ 对话框关闭规则”部分中。可以说,基本上一切都取决于ButtonType已将s添加到DialogPane。在您的示例中,您使用以下预定义类型之一:ButtonType.CANCEL。如果您查看该字段的文档,将会看到:

预先限定的ButtonType,其中显示“取消”,并且具有ButtonBar.ButtonDataButtonBar.ButtonData.CANCEL_CLOSE

如果你看一下文件ButtonData.CANCEL_CLOSE,你会看到:

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

是取消按钮: True

至少对于默认实现而言,这意味着为之Button创建的ButtonType.CANCEL将是取消按钮。换句话说,Buttonwill将其cancelButton属性设置为true。这就是允许您Dialog通过按键关闭a 的原因Esc

注意:这是DialogPane#createButton(ButtonType)负责创建适当按钮的方法(可以重写以进行自定义)。尽管该方法的返回类型是Node正常的,如所记录的那样,通常返回的实例Button

TextFormatter

(核心)JavaFX中的每个控件都具有三个组件:控件类,外观类和行为类。后一类负责处理用户输入,例如鼠标和按键事件。在这种情况下,我们关心TextInputControlBehaviorTextFieldBehavior;前者是后者的超类。

注意:与在JavaFX 9中成为公共API的外观类不同,从JavaFX 12.0.2开始,行为类仍是私有API。下面描述的大部分内容都是实现细节。

所述TextInputControlBehavior类寄存器的EventHandler该反作用于Esc被按下的键,调用cancelEdit(KeyEvent)同一类别的方法。如果有一个,此方法的所有基本实现都将转发KeyEventTextInputControl的父级(由于某种未知的原因,导致两个事件分派周期)。但是,TextFieldBehavior该类将重写此方法:

@Override
protected void cancelEdit(KeyEvent event) {
    TextField textField = getNode();
    if (textField.getTextFormatter() != null) {
        textField.cancelEdit();
        event.consume();
    } else {
        super.cancelEdit(event);
    }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,a的存在TextFormatter会导致a KeyEvent被无条件消耗。这将阻止事件到达Scene,取消按钮不会被触发,因此DialogEsc按住时按住键时不会关闭TextFieldTextFormatter如前所述,当不存在任何超级实现时,只需将事件转发给父级。

呼叫暗示了这种现象的原因TextInputControl#cancelEdit()。该方法具有形式为的“姐妹方法” TextInputControl#commitValue()。如果您查看这两种方法的文档,将会看到:

如果当前正在编辑该字段,则此调用会将文本设置为最后提交的值。

和:

提交当前文本并将其转换为值。

分别。不幸的是,这并不能解释太多,但是如果您看一下实现,它们的目的就很清楚了。一个TextFormatter具有value其特性实时更新,同时输入到TextField。取而代之的是,该值仅在提交后才更新(例如,按Enter)。反之亦然。通过取消编辑(例如,按Esc)可以将当前文本恢复为当前值。

注意:String和任意类型的对象之间的转换由与StringConverter关联的对象处理TextFormatter

当存在时TextFormatter,取消编辑的行为被视为消耗事件的场景。我想这是有道理的。但是,即使没有什么可以取消的事件,事件仍然被消耗掉,这对我来说意义不大。


一个办法

解决此问题的一种方法是使用反射深入内部,如kleopatra的答案所示。另一种方法是添加事件过滤器的TextField或的一些祖先TextField是关闭Dialog,当Esc按下键。

textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
    if (event.getCode() == KeyCode.ESCAPE) {
        event.consume();
        dialog.close();
    }
});
Run Code Online (Sandbox Code Playgroud)

如果您要包括取消编辑行为(取消而不取消就取消),那么只有在Dialog没有要取消的编辑时才应关闭。查看kleopatra的答案,看看如何确定是否需要取消。如果有要取消的内容,请不要使用该事件,也不要关闭该事件Dialog。如果没有要取消的内容,请执行与上面的代码相同的操作(即消耗并关闭)。

使用事件过滤器是“推荐的方式”吗?这肯定是有效的方法。JavaFX 与大多数(如果不是全部)主流UI工具包一样是事件驱动的。专门针对JavaFX,这意味着对Events或观察Observable[Value]s进行无效/更改。在“ JavaFX之上”构建的框架可以添加自己的机制。由于问题是我们不希望发生的事件被消耗,因此添加您自己的处理程序以实现所需的行为是有效的。

  • 真正的好答案是,没有意识到肠子中的特定调整:)对于有人允许/有足够的勇气涉足浑水的人们,另一种方法是替换行为所安装的字段inputMap中的键绑定。肮脏有两个方面:a)需要反射性地访问皮肤中的私有行为字段b)更改作为内部类的inputMap(不幸的是,当皮肤移到公开位置时,它没有进入公共范围) (2认同)