我有一个带有几个UI元素的自定义对话框。一些文本字段用于numeric input。当按下逃逸键并且焦点位于任何数字文本字段上时,此对话框不会关闭。当焦点位于没有此自定义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
上时,如果按了退出键,如何关闭对话框?
答案 0 :(得分:4)
问题已经有一个excellent answer,没有要添加的内容。只是想演示如何调整行为的InputMap来注入/替换我们自己的映射(作为我的评论的后续内容)。注意:以反射方式访问皮肤的行为(私有最终字段)并使用内部api是肮脏的(Behavior / InputMap尚未将其公开)。
正如Slaw所指出的,如果TextField安装了TextFormatter,则是防止ESCAPE冒泡到取消按钮的行为。 IMO,在这种情况下,它的行为不是多余的,只是超调:当且仅当没有其他人使用它来更改任何输入节点的状态时,才应在ESCAPE / ENTER上触发cancel / default按钮(我对已消耗-令人尴尬地对我目前找不到的一般UX准则进行了一些研究...)
适用于同时包含带有textFormatter的textField和取消按钮的表单(又称:isCancelButton为true)
行为中的cancelEdit的实现不能区分这两个状态,但是总是消耗它。下面的示例实现了预期的行为(至少对我而言)。它有
请注意,这是一个PoC:不属于帮助程序,而是属于自定义皮肤(至少,理想情况下应通过行为来完成)。而且它缺少对ENTER ..的类似支持,因为它需要考虑actionHandlers(这种行为试图fails to achieve,但它要考虑到),所以会涉及更多的问题。
要测试示例:
示例代码:
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());
}
答案 1 :(得分:1)
您可以将EventHandler
添加到TextField
。
id.addEventFilter(KeyEvent.KEY_PRESSED, evt -> {
if (evt.getCode() == KeyCode.ESCAPE) {
evt.consume();
dialog.close();
}
});
您也可以将此EventHandler
添加到您的DialogPane
的{{1}}(或者实际上是Dialog
的任何父母/祖先)中。
将id
添加到祖先的原因是JavaFX调度事件的方式。您可以在JavaFX教程here中阅读有关事件处理的更多信息。
当您通过EventHandler
向EventHandler
的祖先添加addEventFilter
时,您是在id
实际到达Event
之前对其进行拦截。在上方的id
中,您仅在EventHandler
是由用户按下KeyEvent
键引起的情况下才做出反应。如果它是ESCAPE
键,则关闭ESCAPE
并使用Dialog
(这将停止对KeyEvent
的更多处理)。如果不是KeyEvent
键,则您什么也不做,而ESCAPE
会照常继续到KeyEvent
。
id
会改变事物这涉及JavaFX的内部类/代码。 JavaFX中的每个TextFormatter
(对于标准库)都有一个Control
和一个Skin
。 Behavior
确定外观(在JavaFX 9中成为公共API),而Skin
确定Behavior
对用户操作的反应(从JavaFX 10开始仍是私有API)。此问题的相关类是Control
,BehaviorBase
和TextInputControlBehavior
(每个类都在其之前扩展了该类)。
TextFieldBehavior
中有一个称为TextInputControlBehavior
的方法。每当由cancelEdit(KeyEvent)
引起的KeyEvent
到达ESCAPE
时,都会调用此方法。基本实现只是在TextInputControl
的父节点上重新触发事件。此“重新触发”会导致消耗TextInputControl
之前发生单独的事件分配周期(针对父对象)。这很重要,因为KeyEvent
会覆盖此基本实现(如下所述)。
在Java 8中,似乎到达TextFieldBehavior
并具有关联操作的所有KeyEvent
都被消耗了。当没有TextInputControl
时,这不是问题,因为事件在消耗之前已“转发”给父对象。但是,TextFormatter
中的覆盖方法仅在没有 TextFieldBehavior
时才“转发”事件。
TextFormatter
这意味着当 是@Override
protected void cancelEdit(KeyEvent event) {
TextField textField = getControl();
if (textField.getTextFormatter() != null) {
textField.cancelEdit();
} else {
forwardToParent(event);
}
}
时,TextFormatter
永远不会冒泡回到KeyEvent
(在“为什么如此重要? “)。
在Java 10中,似乎他们改变了一些东西。现在,到达Scene
且具有关联操作的所有KeyEvent
并没有被异常消耗。这些例外之一是TextInputControl
键。但是,ESCAPE
中的重写方法再次起作用。在这种情况下,方法消耗TextFieldBehavior
,并且永远不会在{em> 是{{1 }} 。
KeyEvent
同样,这意味着当 是KeyEvent
时,TextFormatter
永远不会冒泡回到@Override
protected void cancelEdit(KeyEvent event) {
TextField textField = getNode();
if (textField.getTextFormatter() != null) {
textField.cancelEdit();
event.consume();
} else {
super.cancelEdit(event); // would "forward" the event to the parent
}
}
(下文所述的重要性)。
注意:关于为什么,我不知道这些类是通过这种方式设计的,但我认为是有原因的。。
创建TextFormatter
时,必须将所需的KeyEvent
添加到Scene
中(或者,如果使用Dialog
,则会为您添加它们) 。在您的代码中,您添加的ButtonType
之一是DialogPane
。此预定义的Alert
具有以下ButtonType
:ButtonBar.ButtonData.CANCEL_CLOSE
。 ButtonType.CANCEL
的Javadoc是:
“取消”或“关闭”按钮的标签。
是“取消”按钮:是
按钮订购代码: C
重要的部分是“ 是取消按钮:是”。这意味着ButtonType
创建与ButtonBar.ButtonData
关联的ButtonData
时,其cancelButton
property设置为Dialog
。当您按下Button
时,正是ButtonType
关闭了true
。但是,Button
属性的Javadoc声明(强调我的意思):
“取消按钮”是接收键盘VK_ESC键的按钮,如果场景中没有其他节点消耗它,则。
在JavaFX中处理默认和关闭Dialog
的方式是向ESCAPE
所属的closeButton
添加事件挂钩。仅当Button
发生{strong>冒泡到Scene
时才调用这些挂钩。如上所示,在Button
有KeyEvent
的情况下,Scene
决不会冒泡到KeyEvent
。
答案很简单,是
JavaFX基本上可以通过Scene
对TextField
做出反应,或者通过侦听器对TextFormatter
中的变化做出反应。这是内部完成的方式,也是应用程序开发人员(您)如何将行为编码到UI中的方法。
由于javafx.event.Event
在存在EventHandler
时从未到达(由于被消耗)javafx.beans.Observable
,因此实际上只有一个解决方案。您必须先拦截KeyEvent
,然后再使用自己的行为。关闭Scene
与“取消”相同,如class Javadoc标题为“ 对话框关闭规则”一节中所述。