简体   繁体   English

JavaFX Spinner 空文本 nullpointerexception

[英]JavaFX Spinner empty text nullpointerexception

I have an issue where an editable JavaFX 8 Spinner causes an uncaught NullPointerException if one clears the editor text and commits and then clicks either the increment or decrement button.我有一个问题,如果清除编辑器文本并提交,然后单击递增或递减按钮,可编辑的 JavaFX 8 Spinner会导致未捕获的NullPointerException This is j8u60 j8u77.这是 j8u60 j8u77。 With some luck the increment/decrement button will get stuck in depressed state and the NPE's keep flowing locking up the application.运气好的话,递增/递减按钮会卡在按下状态,并且 NPE 会不断锁定应用程序。

The following code reproduces the issue for me:以下代码为我重现了该问题:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory;
import javafx.stage.Stage;

public class Test extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage aPrimaryStage) throws Exception {
        IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10);
        Spinner<Integer> spinner = new Spinner<>(valueFactory);
        spinner.setEditable(true);
        aPrimaryStage.setScene(new Scene(spinner));
        aPrimaryStage.show();
    }
}

Run it, clear the text, press enter ( NullPointerException ), clicking either increment or decrement button will now also cause NPE.运行它,清除文本,按回车( NullPointerException ),单击增量或减量按钮现在也会导致 NPE。

Can any one confirm that this is a JavaFX bug and suggest a workaround?任何人都可以确认这是一个 JavaFX 错误并提出解决方法吗?

Edit: The exception stack trace编辑:异常堆栈跟踪

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
    at javafx.scene.control.SpinnerValueFactory$IntegerSpinnerValueFactory.lambda$new$215(SpinnerValueFactory.java:475)
    at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
    at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
    at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
    at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
    at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
    at javafx.scene.control.SpinnerValueFactory.setValue(SpinnerValueFactory.java:150)
    at javafx.scene.control.Spinner.lambda$new$210(Spinner.java:139)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Node.fireEvent(Node.java:8411)
    at com.sun.javafx.scene.control.behavior.TextFieldBehavior.fire(TextFieldBehavior.java:179)
    at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callAction(TextInputControlBehavior.java:178)
    at com.sun.javafx.scene.control.behavior.BehaviorBase.callActionForEvent(BehaviorBase.java:218)
    at com.sun.javafx.scene.control.behavior.TextInputControlBehavior.callActionForEvent(TextInputControlBehavior.java:127)
    at com.sun.javafx.scene.control.behavior.BehaviorBase.lambda$new$74(BehaviorBase.java:135)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
    at com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
    at com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Node.fireEvent(Node.java:8411)
    at com.sun.javafx.scene.control.skin.SpinnerSkin.lambda$new$473(SpinnerSkin.java:151)
    at com.sun.javafx.event.CompositeEventHandler$NormalEventFilterRecord.handleCapturingEvent(CompositeEventHandler.java:282)
    at com.sun.javafx.event.CompositeEventHandler.dispatchCapturingEvent(CompositeEventHandler.java:98)
    at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:223)
    at com.sun.javafx.event.EventHandlerManager.dispatchCapturingEvent(EventHandlerManager.java:180)
    at com.sun.javafx.event.CompositeEventDispatcher.dispatchCapturingEvent(CompositeEventDispatcher.java:43)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:52)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
    at com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
    at com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
    at com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
    at javafx.event.Event.fireEvent(Event.java:198)
    at javafx.scene.Scene$KeyHandler.process(Scene.java:3964)
    at javafx.scene.Scene$KeyHandler.access$1800(Scene.java:3910)
    at javafx.scene.Scene.impl_processKeyEvent(Scene.java:2040)
    at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2501)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:197)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:147)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent$353(GlassViewEventHandler.java:228)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:227)
    at com.sun.glass.ui.View.handleKeyEvent(View.java:546)
    at com.sun.glass.ui.View.notifyKey(View.java:966)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)

This is correct and expected behavior for an Integer based Spinner control. 这是基于Integer的Spinner控件的正确和预期行为。

You should either set its Editable property to false, if you do not wish users to edit the values set via the Factory. 如果您不希望用户编辑通过Factory设置的值,则应将其Editable属性设置为false。

Or you should be handling the event raised by the spinner's value property. 或者您应该处理由微调器的value属性引发的事件。

Here's a simple example of how to do so: 以下是如何执行此操作的简单示例:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory;
import javafx.stage.Stage;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class Spin extends Application {
    Spinner<Integer> spinner;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage aPrimaryStage) throws Exception {
        IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10);
        spinner = new Spinner<>(valueFactory);
        spinner.setEditable(true);
        spinner.valueProperty().addListener((observableValue, oldValue, newValue) -> handleSpin(observableValue, oldValue, newValue));

        aPrimaryStage.setScene(new Scene(spinner));
        aPrimaryStage.show();
    }

    private void handleSpin(ObservableValue<?> observableValue, Number oldValue, Number newValue) {
        try {
            if (newValue == null) {
                spinner.getValueFactory().setValue((int)oldValue);
            }
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

This may also assist you, if you wish to use a converter class to help in handling the changes more comprehensively. 如果您希望使用转换器类来帮助更全面地处理更改, 也可以帮助您。

See also the official documentation on the setEditable method ; 另请参阅有关setEditable方法的官方文档;

I had a rummage through the JDK source. 我通过JDK源进行了翻找。

The NPE is thrown from if (newValue < getMin()) { in the listener lambda here: NPE是从if (newValue < getMin()) {在监听器lambda中抛出的:

javafx.scene.control.SpinnerValueFactory.java javafx.scene.control.SpinnerValueFactory.java

    public IntegerSpinnerValueFactory(@NamedArg("min") int min,
                                      @NamedArg("max") int max,
                                      @NamedArg("initialValue") int initialValue,
                                      @NamedArg("amountToStepBy") int amountToStepBy) {
        setMin(min);
        setMax(max);
        setAmountToStepBy(amountToStepBy);
        setConverter(new IntegerStringConverter());

        valueProperty().addListener((o, oldValue, newValue) -> {
            // when the value is set, we need to react to ensure it is a
            // valid value (and if not, blow up appropriately)
            if (newValue < getMin()) { 
                setValue(getMin());
            } else if (newValue > getMax()) {
                setValue(getMax());
            }
        });
        setValue(initialValue >= min && initialValue <= max ? initialValue : min);
    }

presumably newValue is null and the auto unboxing of null throws NPE. 想必newValuenull和自动拆箱null ,抛出NPE。 As the input comes from the editor, I suspect the IntegerStringConverter which is the default converter. 由于输入来自编辑器,我怀疑IntegerStringConverter是默认转换器。

Looking at the implementation here: 看看这里的实现:

javafx.util.converter.IntegerStringConverter javafx.util.converter.IntegerStringConverter

public class IntegerStringConverter extends StringConverter<Integer> {
    /** {@inheritDoc} */
    @Override public Integer fromString(String value) {
        // If the specified value is null or zero-length, return null
        if (value == null) {
            return null;
        }

        value = value.trim();

        if (value.length() < 1) {
            return null;
        }

        return Integer.valueOf(value);
    }

    /** {@inheritDoc} */
    @Override public String toString(Integer value) {
        // If the specified value is null, return a zero-length String
        if (value == null) {
            return "";
        }

        return (Integer.toString(((Integer)value).intValue()));
    }
}

We see that it will happily return null for the empty string, which is kind of reasonable given that there exists no valid value for the input. 我们看到它将为空字符串高兴地返回null ,这是合理的,因为输入没有有效值。

Tracing up the call stack I find where the value is coming from: 跟踪调用堆栈我发现值的来源:

javafx.scene.control.Spinner javafx.scene.control.Spinner

public Spinner() {
    getStyleClass().add(DEFAULT_STYLE_CLASS);
    setAccessibleRole(AccessibleRole.SPINNER);

    getEditor().setOnAction(action -> {
        String text = getEditor().getText();
        SpinnerValueFactory<T> valueFactory = getValueFactory();
        if (valueFactory != null) {
            StringConverter<T> converter = valueFactory.getConverter();
            if (converter != null) {
                T value = converter.fromString(text);
                valueFactory.setValue(value);
            }
        }
    });

The value is set with the value obtained from the converter T value = converter.fromString(text); 使用从转换器T value = converter.fromString(text);获得的值设置该值T value = converter.fromString(text); which presumably is null. 大概是空的。 At this point I believe that the spinner class should check that value is not null and if it is restore the previous value to the editor. 此时我认为spinner类应检查该value是否为null以及是否将以前的值恢复到编辑器。

I am now fairly sure that this is a bug. 我现在相当确定这是一个错误。 Moreover I don't think that a work around with a converter that never returns null is going to work properly as it will only mask the problem and what value should be returned when the value cannot be converted? 此外,我不认为使用永远不会返回null的转换器的工作将会正常工作,因为它只会掩盖问题,并且当值无法转换时应该返回什么值?

Edit: Workaround 编辑:解决方法

Replacing the onAction of the spinner editor to reject invalid input with a "return to valid" policy fixes the issue: 使用“返回有效”策略替换微调器编辑器的onAction以拒绝无效输入可修复此问题:

public static <T> void fixSpinner2(Spinner<T> aSpinner) {
    aSpinner.getEditor().setOnAction(action -> {
        String text = aSpinner.getEditor().getText();
        SpinnerValueFactory<T> factory = aSpinner.getValueFactory();
        if (factory != null) {
            StringConverter<T> converter = factory.getConverter();
            if (converter != null) {
                T value = converter.fromString(text);
                if (null != value) {
                    factory.setValue(value);
                }
                else {
                    aSpinner.getEditor().setText(converter.toString(factory.getValue()));
                }
            }
        }
        action.consume();
    });
}

As opposed to a listener on the valueProperty this avoids triggering other listeners with invalid data. valueProperty上的侦听器相反,这避免了使用无效数据触发其他侦听器。 However this highlights another issue in the spinner class. 然而,这突出了微调器类中的另一个问题。 While the above fixes the issue by returning to a valid value on pressing enter. 虽然上面通过按Enter键返回有效值来解决问题。 Erasing the input without committing (pressing enter) and then pressing increment or decrement will cause the same NPE but with slightly different call stack. 擦除输入而不提交(按Enter键)然后按递增或递减将导致相同的NPE,但调用堆栈略有不同。

Cause: 原因:

public void increment(int steps) {
    SpinnerValueFactory<T> valueFactory = getValueFactory();
    if (valueFactory == null) {
        throw new IllegalStateException("Can't increment Spinner with a null SpinnerValueFactory");
    }
    commitEditorText();
    valueFactory.increment(steps);
}

Decrement is similar, both call into commitEditorText below: 递减类似,都调用下面的commitEditorText

private void commitEditorText() {
    if (!isEditable()) return;
    String text = getEditor().getText();
    SpinnerValueFactory<T> valueFactory = getValueFactory();
    if (valueFactory != null) {
        StringConverter<T> converter = valueFactory.getConverter();
        if (converter != null) {
            T value = converter.fromString(text);
            valueFactory.setValue(value);
        }
    }
}

Notice the copy-paste from the onAction in the constructor: 注意构造函数中onAction的复制粘贴:

    getEditor().setOnAction(action -> {
        String text = getEditor().getText();
        SpinnerValueFactory<T> valueFactory = getValueFactory();
        if (valueFactory != null) {
            StringConverter<T> converter = valueFactory.getConverter();
            if (converter != null) {
                T value = converter.fromString(text);
                valueFactory.setValue(value);
            }
        }
    });

I believe that commitEditorText should be changed to trigger onAction on the editor instead like so: 我相信应该更改commitEditorText以在编辑器上触发onAction ,如下所示:

private void commitEditorText() {
    if (!isEditable()) return;
    getEditor().getOnAction().handle(new ActionEvent(this, this));
}

then the behavior would be consistent and give the editor a chance to handle the input before it goes to the value factory. 然后行为将是一致的,并让编辑器有机会在输入到值工厂之前处理输入。

I would consider this a bug: the IntegerSpinnerValueFactory should properly handle this case. 我认为这是一个错误: IntegerSpinnerValueFactory应该正确处理这种情况。

One workaround is to provide a converter to the spinner value factory that evaluates to a default value if the text value is not valid: 一种解决方法是为微调器值工厂提供converter器,如果文本值无效,则将其converter为默认值:

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class Test extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage aPrimaryStage) throws Exception {
        IntegerSpinnerValueFactory valueFactory = new IntegerSpinnerValueFactory(0, 10);

        valueFactory.setConverter(new StringConverter<Integer>() {

            @Override
            public String toString(Integer object) {
                return object.toString() ;
            }

            @Override
            public Integer fromString(String string) {
                if (string.matches("-?\\d+")) {
                    return new Integer(string);
                }
                // default to 0:
                return 0 ;
            }

        });

        Spinner<Integer> spinner = new Spinner<>(valueFactory);
        spinner.setEditable(true);
        aPrimaryStage.setScene(new Scene(spinner));
        aPrimaryStage.show();
    }
}

这是一个已知的错误,已在Java 9中修复 - 请参阅https://bugs.openjdk.java.net/browse/JDK-8150962

Another answer which worked for me, stopping you from entering any non-number value:另一个对我有用的答案,阻止你输入任何非数字值:

spinnerIndex.editorProperty().getValue().textProperty().addListener(new ChangeListener<String>() {

    private static boolean isInteger(final String s) {
        try {
            @SuppressWarnings("unused")
            int d = Integer.parseInt(s);
            return true;
        }
        catch (NumberFormatException e) {
            return false;
        }
    }

    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
        if (!isInteger(newValue)) {
            final StringProperty sp = (StringProperty)observable;
            sp.set(oldValue);
        }
    }
});

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM