JavaFX - DatePicker 在失去对无效日期的焦点时抛出不可恢复的异常

问题描述 投票:0回答:1

我在尝试扩展 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 并能够解决问题,但意外的异常仍然存在。

我怎样才能通过合理的努力来防止这种行为?

预先感谢您的帮助!

预期行为与提交文本时相同。示例应用程序显示了我尝试查找问题的方法。

javafx datepicker
1个回答
0
投票

这是当前稳定版本(在撰写本文时为版本 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(); } } });
开箱即用地解决了问题。

© www.soinside.com 2019 - 2024. All rights reserved.