首页 > 解决方案 > 从 JavaFX 对话框中的数字文本字段中转义

问题描述

我有一个包含多个 UI 元素的自定义对话框。一些 TextFields 用于数字输入。当按下转义键并且焦点位于任何数字文本字段上时,此对话框不会关闭。当焦点位于没有此自定义 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 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);
    }
}

在焦点打开时按下转义键时如何关闭对话框id

标签: javafx

解决方案


问题

在提供解决方案之前,我认为了解为什么拥有一个TextFormatter似乎会改变Dialog. 如果这对您不重要,请随时跳到答案的末尾。

取消按钮

根据的文档Button取消按钮是:

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

这句话的结尾是重要的部分。取消按钮以及默认按钮的实现方式是通过向所属的注册一个加速器。这些加速器只有在适当的冒泡到. 如果事件在到达 之前被消费,则不会调用加速器。SceneButtonKeyEvent SceneScene

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

对话框

ADialog有一些关于如何以及何时关闭的规则。这些规则记录在这里,在对话结束规则部分。可以说,基本上一切都取决于ButtonType添加到DialogPane. 在您的示例中,您使用其中一种预定义类型:ButtonType.CANCEL. 如果您查看该字段的文档,您会看到:

ButtonType显示“取消”并具有 的预定ButtonBar.ButtonDataButtonBar.ButtonData.CANCEL_CLOSE

如果您查看的文档ButtonData.CANCEL_CLOSE您会看到:

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

是取消按钮: True

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

注意:它是DialogPane#createButton(ButtonType)负责创建适当按钮的方法(并且可以被覆盖以进行自定义)。虽然该方法的返回类型Node是典型的,如文档所述,返回一个Button.

文本格式化程序

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

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

该类TextInputControlBehavior注册一个对被按下的键EventHandler做出反应的,调用同一类的方法。此方法的所有基本实现都是将 转发给的父级,如果它有一个 - 由于某些未知(对我而言)原因导致两个事件分派周期。但是,该类会覆盖此方法:EsccancelEdit(KeyEvent)KeyEventTextInputControlTextFieldBehavior

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

如您所见, a 的存在TextFormatter导致KeyEvent无条件地消耗。这会阻止事件到达Scene,取消按钮不会被触发,因此当有焦点时按下键Dialog时 不会关闭。当没有调用超级实现时,如前所述,它只是将事件转发给父级。EscTextFieldTextFormatter

对 的调用暗示了这种行为的原因TextInputControl#cancelEdit()。该方法有一个形式为 的“姊妹方法” TextInputControl#commitValue()。如果您查看这两种方法的文档,您会看到:

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

和:

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

分别。不幸的是,这并不能解释太多,但是如果您查看实现,它们的目的就会变得清晰。ATextFormatter有一个value属性,输入TextField. 相反,该值仅在提交时更新(例如通过按Enter)。反之亦然;当前文本可以通过取消编辑(例如按Esc)恢复为当前值。

注意:String与任意类型的对象之间的转换由StringConverterTextFormatter.

当有 时TextFormatter,取消编辑的行为被认为是一个消耗事件的场景。我想这是有道理的。然而,即使没有什么可以取消的事件仍然被消耗——这对我来说没有多大意义。


一个办法

解决此问题的一种方法是使用反射深入研究内部结构,如kleopatra 的回答所示。另一种选择是将事件过滤器添加到在按下键时关闭的TextField或某些祖先。TextFieldDialogEsc

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

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

使用事件过滤器是“推荐的方式”吗?这当然是一种有效的方式。JavaFX与大多数(如果不是全部)主流 UI 工具包一样是事件驱动的。对于 JavaFX,这意味着对Events 做出反应或观察Observable[Value]s 的失效/更改。“建立在”JavaFX 之上的框架可能会添加自己的机制。由于问题是我们不希望使用的事件,因此添加您自己的处理程序以实现所需的行为是有效的。


推荐阅读