我在尝试扩展 DatePicker 控件时偶然发现了一个烦人的问题(JavaFX SDK 21.0.02)。
当在文本字段中输入无效日期并且焦点丢失而未事先按 Enter 键时,您将收到
Exception in thread "JavaFX Application Thread" java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor
按回车提交无效输入时,不会抛出此异常。
DatePicker 构造函数安装一个焦点侦听器,该侦听器应该通过使用当前转换器进行验证来处理这种情况。
focusedProperty().addListener(o -> {
if (!isFocused()) {
commitValue();
}
});
和
public final void commitValue() {
if (!isEditable()) {
return;
}
String text = getEditor().getText();
StringConverter<LocalDate> converter = getConverter();
if (converter != null) {
LocalDate value = converter.fromString(text);
setValue(value);
}
}
不幸的是,在到达 fromString 方法之前就引发了异常。它发生在DatePicker默认Chronolgy的resolveDate方法的深处。
为了演示此行为,这里有一个示例应用程序:
package application;
import java.text.DateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology;
import java.time.chrono.Era;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.ValueRange;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javafx.application.Application;
import javafx.stage.Stage;
import javafx.util.converter.LocalDateStringConverter;
import javafx.scene.Scene;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.GERMAN);
try {
Locale.setDefault(Locale.GERMAN);
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
FlowPane flowPane = new FlowPane();
NewPicker datePicker = new NewPicker();
datePicker.focusedProperty().addListener((v, o, n)->{
if (!n) System.out.println("Focus lost on DatePicker");
});
datePicker.getEditor().focusedProperty().addListener((v, o, n)->{
if (!n) System.out.println("Focus lost on DatePickers TextField");
});
Chronology chronology = datePicker.getChronology();
datePicker.setChronology(new Chronology() {
@Override public ChronoLocalDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) {
System.out.println("Reached resolveDate with fieldValues " + fieldValues +
" and resolverStyle " + resolverStyle);
ChronoLocalDate test = null;
try {
test = chronology.resolveDate(fieldValues, resolverStyle);
} catch(Exception e) {
System.out.println("Exception in resolveDate with fieldValues " + fieldValues +
" and resolverStyle " + resolverStyle);
}
return test;
}
@Override public ValueRange range(ChronoField field) {
return chronology.range(field); }
@Override public int prolepticYear(Era era, int yearOfEra) {
return chronology.prolepticYear(era, yearOfEra); }
@Override public boolean isLeapYear(long prolepticYear) {
return chronology.isLeapYear(prolepticYear); }
@Override public String getId() {
return chronology.getId(); }
@Override public String getCalendarType() {
return chronology.getCalendarType(); }
@Override public List<Era> eras() {
return chronology.eras(); }
@Override public Era eraOf(int eraValue) {
return chronology.eraOf(eraValue); }
@Override public ChronoLocalDate dateYearDay(int prolepticYear, int dayOfYear) {
return chronology.dateYearDay(prolepticYear, dayOfYear); }
@Override public ChronoLocalDate dateEpochDay(long epochDay) {
return chronology.dateEpochDay(epochDay); }
@Override public ChronoLocalDate date(int prolepticYear, int month, int dayOfMonth) {
return chronology.date(prolepticYear, month, dayOfMonth); }
@Override public ChronoLocalDate date(TemporalAccessor temporal) {
return chronology.date(temporal);
}
@Override public int compareTo(Chronology other) {
return chronology.compareTo(other); }
});
datePicker.getEditor().setTextFormatter(new TextFormatter<LocalDate>(new LocalDateStringConverter() {
@Override public LocalDate fromString(String value) {
System.out.println("Reached fromString with value " + value);
try {
if (value!=null && !value.isBlank())
System.out.println("Parsed: " + df.parse(value));
} catch (Exception e) {
System.out.println("Exception in fromString");
//datePicker.setValue(LocalDate.now());
datePicker.getEditor().setText(df.format(Date.from(Instant.now())));
return LocalDate.now();
}
return super.fromString(value);
}
@Override public String toString(LocalDate value) {
System.out.println("Reached toString");
return super.toString(value);
}
}));
flowPane.getChildren().add(datePicker);
flowPane.getChildren().add(new TextField());
root.getChildren().add(flowPane);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private class NewPicker extends DatePicker {
}
}
启动应用程序并按 Enter 键输入 1.13.2024(德国日期)。 然后输入 1.13.2024 并按 Tab 键。
需要明确的是:最终您将到达 fromString 并能够解决问题,但意外的异常仍然存在。
我怎样才能通过合理的努力来防止这种行为?
预先感谢您的帮助!
预期行为与提交文本时相同。示例应用程序显示了我尝试查找问题的方法。
这是当前稳定版本(在撰写本文时为版本 21.0.2)中的一个错误。它已在当前 ea 版本 (
22-ea+27
) 中修复,该版本将于 2024 年 3 月发布。
在 21.0.2 上运行会产生以下堆栈跟踪:
Exception in thread "JavaFX Application Thread" java.time.DateTimeException: Unable to obtain LocalDate from TemporalAccessor: {DayOfMonth=1},org.jamesd.examples.dialogsize.Main$1@5eea803f of type java.time.format.Parsed
at java.base/java.time.LocalDate.from(LocalDate.java:403)
at [email protected]/javafx.util.converter.LocalDateTimeStringConverter$LdtConverter.fromString(LocalDateTimeStringConverter.java:206)
at [email protected]/javafx.util.converter.LocalDateStringConverter.fromString(LocalDateStringConverter.java:134)
at [email protected]/javafx.util.converter.LocalDateStringConverter.fromString(LocalDateStringConverter.java:45)
at [email protected]/javafx.scene.control.DatePicker.commitValue(DatePicker.java:445)
at [email protected]/javafx.scene.control.DatePicker.lambda$new$2(DatePicker.java:150)
at [email protected]/com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:360)
at [email protected]/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:91)
at [email protected]/javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:78)
at [email protected]/javafx.scene.Node$FocusPropertyBase.notifyListeners(Node.java:8142)
at [email protected]/javafx.scene.Node$17.notifyListeners(Node.java:8214)
at [email protected]/javafx.scene.Node.notifyFocusListeners(Node.java:8163)
at [email protected]/javafx.scene.Scene$FocusOwnerProperty.invalidated(Scene.java:2253)
at [email protected]/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at [email protected]/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
at [email protected]/javafx.scene.Scene.setFocusOwner(Scene.java:2304)
at [email protected]/javafx.scene.Scene.requestFocus(Scene.java:2201)
at [email protected]/javafx.scene.Node.requestFocusVisible(Node.java:8423)
at [email protected]/javafx.scene.Node$1.requestFocusVisible(Node.java:625)
at [email protected]/com.sun.javafx.scene.NodeHelper.requestFocusVisible(NodeHelper.java:312)
at [email protected]/com.sun.javafx.scene.traversal.TopMostTraversalEngine.focusAndNotify(TopMostTraversalEngine.java:113)
at [email protected]/com.sun.javafx.scene.traversal.TopMostTraversalEngine.trav(TopMostTraversalEngine.java:106)
at [email protected]/javafx.scene.Scene.traverse(Scene.java:2159)
at [email protected]/javafx.scene.Node.traverse(Node.java:8437)
at [email protected]/javafx.scene.Node$1.traverse(Node.java:526)
at [email protected]/com.sun.javafx.scene.NodeHelper.traverse(NodeHelper.java:238)
at [email protected]/com.sun.javafx.scene.control.behavior.FocusTraversalInputMap.traverse(FocusTraversalInputMap.java:97)
at [email protected]/com.sun.javafx.scene.control.behavior.FocusTraversalInputMap.traverseNext(FocusTraversalInputMap.java:137)
at [email protected]/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
at [email protected]/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:247)
at [email protected]/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at [email protected]/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:232)
at [email protected]/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:189)
at [email protected]/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at [email protected]/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at [email protected]/javafx.event.Event.fireEvent(Event.java:198)
at [email protected]/javafx.scene.Node.fireEvent(Node.java:8875)
at [email protected]/javafx.scene.control.skin.ComboBoxPopupControl.lambda$new$3(ComboBoxPopupControl.java:173)
at [email protected]/com.sun.javafx.scene.control.ListenerHelper$EvHa.handle(ListenerHelper.java:508)
at [email protected]/com.sun.javafx.event.CompositeEventHandler$NormalEventFilterRecord.handleCapturingEvent(CompositeEventHandler.java:321)
at [email protected]/com.sun.javafx.event.CompositeEventHandler.dispatchCapturingEvent(CompositeEventHandler.java:98)
at [email protected]/com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:219)
at [email protected]/com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:178)
at [email protected]/com.sun.javafx.event.CompositeEventDispatcher.dispatchCapturingEvent(CompositeEventDispatcher.java:43)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:52)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at [email protected]/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at [email protected]/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at [email protected]/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at [email protected]/javafx.event.Event.fireEvent(Event.java:198)
at [email protected]/javafx.scene.Scene.processKeyEvent(Scene.java:2194)
at [email protected]/javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2715)
at [email protected]/com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:218)
at [email protected]/com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:150)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:400)
at [email protected]/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$1(GlassViewEventHandler.java:250)
at [email protected]/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:424)
at [email protected]/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:249)
at [email protected]/com.sun.glass.ui.View.handleKeyEvent(View.java:542)
at [email protected]/com.sun.glass.ui.View.notifyKey(View.java:966)
您可以从堆栈跟踪中看到,使用日期选择器的
focusedProperty
注册的侦听器通过您发布的代码调用 LocalDateStringConverter.fromString()
focusedProperty().addListener(o -> {
if (!isFocused()) {
commitValue();
}
});
和
public final void commitValue() {
if (!isEditable()) {
return;
}
String text = getEditor().getText();
StringConverter<LocalDate> converter = getConverter();
if (converter != null) {
LocalDate value = converter.fromString(text);
setValue(value);
}
}
如果文本无效,这里的(默认)转换器会抛出异常。因此,最简单的修复方法就是更换转换器。您已经定义了一个(但未安装),但您需要修改它,以便如果解析时引发异常,它会返回一个默认值(看来您希望这是
LocalDate.now()
)。将旧版 java.text.DateFormat
与新的 java.time
API 混合使用可能不是一个好主意。
LocalDateStringConverter localDateStringConverter = new LocalDateStringConverter() {
@Override
public LocalDate fromString(String value) {
System.out.println("Reached fromString with value " + value);
try {
return super.fromString(value);
} catch (Exception e) {
System.out.println("Exception in fromString");
return LocalDate.now();
}
}
@Override
public String toString(LocalDate value) {
System.out.println("Reached toString");
return super.toString(value);
}
};
然后安装转换器。 (据我了解,默认情况下,文本格式化程序将使用您安装的转换器,因此无需另外执行此操作。)
datePicker.setConverter(localDateStringConverter);
// datePicker.getEditor().setTextFormatter(new TextFormatter<LocalDate>(localDateStringConverter));
这是示例代码的完整版本,其中包含修复程序(还删除了旧版 API):
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
import javafx.util.converter.LocalDateStringConverter;
import java.time.LocalDate;
import java.time.chrono.ChronoLocalDate;
import java.time.chrono.Chronology;
import java.time.chrono.Era;
import java.time.format.ResolverStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalField;
import java.time.temporal.ValueRange;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class MainFixed extends Application {
@Override
public void start(Stage primaryStage) {
try {
Locale.setDefault(Locale.GERMAN);
BorderPane root = new BorderPane();
Scene scene = new Scene(root,400,400);
// scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
FlowPane flowPane = new FlowPane();
NewPicker datePicker = new NewPicker();
datePicker.focusedProperty().addListener((v, o, n)->{
if (!n) System.out.println("Focus lost on DatePicker");
});
datePicker.getEditor().focusedProperty().addListener((v, o, n)->{
if (!n) System.out.println("Focus lost on DatePickers TextField");
});
Chronology chronology = datePicker.getChronology();
datePicker.setChronology(new Chronology() {
@Override public ChronoLocalDate resolveDate(Map<TemporalField, Long> fieldValues, ResolverStyle resolverStyle) {
System.out.println("Reached resolveDate with fieldValues " + fieldValues +
" and resolverStyle " + resolverStyle);
ChronoLocalDate test = null;
try {
test = chronology.resolveDate(fieldValues, resolverStyle);
} catch(Exception e) {
System.out.println("Exception in resolveDate with fieldValues " + fieldValues +
" and resolverStyle " + resolverStyle);
}
return test;
}
@Override public ValueRange range(ChronoField field) {
return chronology.range(field); }
@Override public int prolepticYear(Era era, int yearOfEra) {
return chronology.prolepticYear(era, yearOfEra); }
@Override public boolean isLeapYear(long prolepticYear) {
return chronology.isLeapYear(prolepticYear); }
@Override public String getId() {
return chronology.getId(); }
@Override public String getCalendarType() {
return chronology.getCalendarType(); }
@Override public List<Era> eras() {
return chronology.eras(); }
@Override public Era eraOf(int eraValue) {
return chronology.eraOf(eraValue); }
@Override public ChronoLocalDate dateYearDay(int prolepticYear, int dayOfYear) {
return chronology.dateYearDay(prolepticYear, dayOfYear); }
@Override public ChronoLocalDate dateEpochDay(long epochDay) {
return chronology.dateEpochDay(epochDay); }
@Override public ChronoLocalDate date(int prolepticYear, int month, int dayOfMonth) {
return chronology.date(prolepticYear, month, dayOfMonth); }
@Override public ChronoLocalDate date(TemporalAccessor temporal) {
return chronology.date(temporal);
}
@Override public int compareTo(Chronology other) {
return chronology.compareTo(other); }
});
LocalDateStringConverter localDateStringConverter = new LocalDateStringConverter() {
@Override
public LocalDate fromString(String value) {
System.out.println("Reached fromString with value " + value);
try {
return super.fromString(value);
} catch (Exception e) {
System.out.println("Exception in fromString");
return LocalDate.now();
}
}
@Override
public String toString(LocalDate value) {
System.out.println("Reached toString");
return super.toString(value);
}
};
datePicker.setConverter(localDateStringConverter);
// datePicker.getEditor().setTextFormatter(new TextFormatter<LocalDate>(localDateStringConverter));
flowPane.getChildren().add(datePicker);
flowPane.getChildren().add(new TextField());
root.getChildren().add(flowPane);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
private class NewPicker extends DatePicker {
}
}
如上所述,这是版本 21 及更早版本中的错误。 21.0.2 的源代码可以在这里检索到
focusedProperty().addListener(o -> {
if (!isFocused()) {
commitValue();
}
});
当前开发版本的源代码(于 2024 年 2 月 6 日检索) 有
focusedProperty().addListener(o -> {
if (!isFocused()) {
try {
commitValue();
} catch (Exception e) {
cancelEdit();
}
}
});
开箱即用地解决了问题。