JavaFX Spinner 空文本空指针异常
JavaFX Spinner empty text nullpointerexception
我遇到一个问题,如果清除编辑器文本并提交然后单击递增或递减按钮,可编辑的 JavaFX 8 Spinner
会导致未捕获 NullPointerException
。这是 j8u60j8u77。运气好的话,increment/decrement 按钮会卡在按下状态,NPE 会继续锁定应用程序。
以下代码为我重现了该问题:
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();
}
}
运行 它,清除文本,按回车键 (NullPointerException
),点击递增或递减按钮现在也会导致 NPE。
任何人都可以确认这是一个 JavaFX 错误并提出解决方法吗?
编辑:异常堆栈跟踪
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.SpinnerValueFactory$IntegerSpinnerValueFactory.lambda$new5(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$new0(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(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$new3(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.access00(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$handleKeyEvent3(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$null8(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
我会认为这是一个错误:IntegerSpinnerValueFactory
应该正确处理这种情况。
一种解决方法是向微调器值工厂提供一个 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();
}
}
我翻遍了 JDK 来源。
NPE 是从侦听器 lambda 中的 if (newValue < getMin()) {
抛出的:
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);
}
大概 newValue
是 null
并且 null
的自动拆箱抛出 NPE。由于输入来自编辑器,我怀疑 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()));
}
}
我们看到它很乐意 return null
用于空字符串,考虑到输入不存在有效值,这是合理的。
跟踪调用堆栈,我找到了值的来源:
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);
}
}
});
该值设置为从转换器 T value = converter.fromString(text);
获得的值,该值可能为空。在这一点上,我相信微调器 class 应该检查 value
不是 null
并且如果它是将以前的值恢复到编辑器。
我现在相当确定这是一个错误。此外,我认为使用永远不会 return 为 null 的转换器的变通方法不会正常工作,因为它只会掩盖问题以及当值不能为 return 时应该编辑什么值已转换?
编辑:解决方法
将微调器编辑器的 onAction
替换为“return 有效”策略以拒绝无效输入修复了该问题:
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();
});
}
与 valueProperty
上的侦听器相反,这避免了使用无效数据触发其他侦听器。然而,这突出了微调器中的另一个问题 class。虽然上面通过 return 在按 enter 时输入有效值来解决问题。在不提交(按回车键)的情况下擦除输入,然后按递增或递减将导致相同的 NPE,但调用堆栈略有不同。
原因:
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);
}
自减类似,都调用到下面的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);
}
}
}
注意构造函数中 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);
}
}
});
我认为 commitEditorText
应该更改为在编辑器上触发 onAction
而不是像这样:
private void commitEditorText() {
if (!isEditable()) return;
getEditor().getOnAction().handle(new ActionEvent(this, this));
}
那么行为将是一致的,并让编辑器有机会在进入值工厂之前处理输入。
这是基于整数的 Spinner 控件的正确和预期行为。
如果您不希望用户编辑通过工厂设置的值,您应该将其可编辑 属性 设置为 false。
或者您应该处理微调器的值 属性 引发的事件。
下面是一个简单的示例:
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());
}
}
}
也可以帮助您,如果您希望使用转换器 class 来帮助更全面地处理更改。
的官方文档
这是一个已知错误,已在 Java 9 中修复 - 请参阅 https://bugs.openjdk.java.net/browse/JDK-8150962
另一个对我有用的答案,阻止你输入任何非数字值:
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);
}
}
});
我遇到一个问题,如果清除编辑器文本并提交然后单击递增或递减按钮,可编辑的 JavaFX 8 Spinner
会导致未捕获 NullPointerException
。这是 j8u60j8u77。运气好的话,increment/decrement 按钮会卡在按下状态,NPE 会继续锁定应用程序。
以下代码为我重现了该问题:
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();
}
}
运行 它,清除文本,按回车键 (NullPointerException
),点击递增或递减按钮现在也会导致 NPE。
任何人都可以确认这是一个 JavaFX 错误并提出解决方法吗?
编辑:异常堆栈跟踪
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException
at javafx.scene.control.SpinnerValueFactory$IntegerSpinnerValueFactory.lambda$new5(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$new0(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(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$new3(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.access00(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$handleKeyEvent3(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$null8(WinApplication.java:191)
at java.lang.Thread.run(Thread.java:745)
我会认为这是一个错误:IntegerSpinnerValueFactory
应该正确处理这种情况。
一种解决方法是向微调器值工厂提供一个 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();
}
}
我翻遍了 JDK 来源。
NPE 是从侦听器 lambda 中的 if (newValue < getMin()) {
抛出的:
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);
}
大概 newValue
是 null
并且 null
的自动拆箱抛出 NPE。由于输入来自编辑器,我怀疑 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()));
}
}
我们看到它很乐意 return null
用于空字符串,考虑到输入不存在有效值,这是合理的。
跟踪调用堆栈,我找到了值的来源:
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);
}
}
});
该值设置为从转换器 T value = converter.fromString(text);
获得的值,该值可能为空。在这一点上,我相信微调器 class 应该检查 value
不是 null
并且如果它是将以前的值恢复到编辑器。
我现在相当确定这是一个错误。此外,我认为使用永远不会 return 为 null 的转换器的变通方法不会正常工作,因为它只会掩盖问题以及当值不能为 return 时应该编辑什么值已转换?
编辑:解决方法
将微调器编辑器的 onAction
替换为“return 有效”策略以拒绝无效输入修复了该问题:
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();
});
}
与 valueProperty
上的侦听器相反,这避免了使用无效数据触发其他侦听器。然而,这突出了微调器中的另一个问题 class。虽然上面通过 return 在按 enter 时输入有效值来解决问题。在不提交(按回车键)的情况下擦除输入,然后按递增或递减将导致相同的 NPE,但调用堆栈略有不同。
原因:
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);
}
自减类似,都调用到下面的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);
}
}
}
注意构造函数中 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);
}
}
});
我认为 commitEditorText
应该更改为在编辑器上触发 onAction
而不是像这样:
private void commitEditorText() {
if (!isEditable()) return;
getEditor().getOnAction().handle(new ActionEvent(this, this));
}
那么行为将是一致的,并让编辑器有机会在进入值工厂之前处理输入。
这是基于整数的 Spinner 控件的正确和预期行为。
如果您不希望用户编辑通过工厂设置的值,您应该将其可编辑 属性 设置为 false。
或者您应该处理微调器的值 属性 引发的事件。
下面是一个简单的示例:
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());
}
}
}
这是一个已知错误,已在 Java 9 中修复 - 请参阅 https://bugs.openjdk.java.net/browse/JDK-8150962
另一个对我有用的答案,阻止你输入任何非数字值:
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);
}
}
});