在JavaFX Spinner中手动输入文本不会更新值(除非用户按下ENTER键)

32
似乎Spinner控件在用户没有明确按下回车键之前不会更新手动输入的值。 因此,他们可以输入一个值(不按回车键),退出控件并提交表单,而在Spinner中显示的值不是Spinner的值,而是旧值。
我的想法是添加一个丢失焦点事件的监听器,但我无法找到一种访问输入值的方法?
spinner.focusedProperty().addListener((observable, oldValue, newValue) -> 
{
    //if focus lost
    if(!newValue)
    {
        //somehow get the text the user typed in?
    }
});

这是奇怪的行为,它似乎违反了GUI旋转按钮控件的约定。

7个回答

36

很遗憾,Spinner并不如预期那样运作:在大多数操作系统中,它应该在焦点丢失时提交编辑后的值。更不幸的是,它没有提供任何配置选项来轻松地使其按照预期运作。

因此,我们必须手动在监听器中将值提交到focusedProperty。好消息是,Spinner已经有了执行此操作的代码-虽然它是私有的,但我们可以复制粘贴它。

/**
 * c&p from Spinner
 */
private <T> void commitEditorText(Spinner<T> spinner) {
    if (!spinner.isEditable()) return;
    String text = spinner.getEditor().getText();
    SpinnerValueFactory<T> valueFactory = spinner.getValueFactory();
    if (valueFactory != null) {
        StringConverter<T> converter = valueFactory.getConverter();
        if (converter != null) {
            T value = converter.fromString(text);
            valueFactory.setValue(value);
        }
    }
}

// useage in client code
spinner.focusedProperty().addListener((s, ov, nv) -> {
    if (nv) return;
    //intuitive method on textField, has no effect, though
    //spinner.getEditor().commitValue(); 
    commitEditorText(spinner);
});

请注意,这里有一个方法。
textField.commitValue()

我本来希望你能提交没有效果的值,但事实上,它只更新了textFormatter的值(最终版本!),如果有的话。即使您使用文本格式验证:textFormatter for validation在Spinner中不起作用。可能是由于缺少某些内部侦听器或者Spinner尚未更新到相对较新的api - 虽然没有深入研究。


更新

当更多地尝试 TextFormatter 时,我注意到格式化程序确保在失去焦点时进行提交

当控件失去焦点或被提交时更新值(仅适用于TextField)

这确实像所述那样正常工作,我们可以添加监听器到格式化程序的valueProperty,以便在提交值时得到通知:

TextField field = new TextField();
TextFormatter fieldFormatter = new TextFormatter(
      TextFormatter.IDENTITY_STRING_CONVERTER, "initial");
field.setTextFormatter(fieldFormatter);
fieldFormatter.valueProperty().addListener((s, ov, nv) -> {
    // do stuff that needs to be done on commit
} );

提交触发器:

  • 用户按下ENTER键
  • 控制失去焦点
  • 通过编程方式调用field.setText(这是未经记录的行为!)

回到Spinner:我们可以利用格式化程序值的失去焦点提交行为来强制提交spinnerFactory的值。类似于以下内容:

// normal setup of spinner
SpinnerValueFactory factory = new IntegerSpinnerValueFactory(0, 10000, 0);
spinner.setValueFactory(factory);
spinner.setEditable(true);
// hook in a formatter with the same properties as the factory
TextFormatter formatter = new TextFormatter(factory.getConverter(), factory.getValue());
spinner.getEditor().setTextFormatter(formatter);
// bidi-bind the values
factory.valueProperty().bindBidirectional(formatter.valueProperty());

请注意,编辑(无论是键入还是以编程方式替换/追加/粘贴文本)不会触发提交 - 因此,如果需要在文本更改时提交,请不要使用此方法。

对于那些在寻找为什么按下回车键后他们的Spinner没有更新的人:你可能正在使用一个ChangeListener - 它只有在值真正改变时才会激活,而JavaFX有时似乎会出错。尝试使用一个InvalidationListener。 - Ondrej Skopek
@kleopatra,希望您能收到这条评论通知。我对Sergio下面的答案有疑问,您有什么想法吗?它似乎是一个简短而甜美的解决方案,感觉太好了,以至于让人怀疑。它只比您的答案新一年,如果它真的那么好,我会期望它现在已经超过了您的答案分数。Sergio的答案是否存在需要注意的问题?还是人们一看到您的解决方案就停止滚动了?我不认为我在尝试在那个答案下标记您时起作用了。 - pateksan
@kleopatra,谢谢。我还有另一个问题(如果我把你拖进一个旧问题中,对不起):当你说“我们可以在编辑时拒绝inc/dec”时,我有点困惑?拒绝inc/dec似乎与OP的问题无关。这是Sergio代码的副作用吗?还是实现OP原始目标的另一种方法?此外,我还是新手,但是如果您将您的评论(刚才上面的那个)放在Sergio的答案下面,它可能对每个人都更有用-如果向具有2 ^ 8个声望的人建议这样做很粗鲁,我很抱歉。 - pateksan
@pateksan _拒绝inc / dec_是如何实现inc的其他方式的示例 - 未指定的内容不存在 :) 不,我不会评论其他答案,已经说得足够了。 - kleopatra
@kleopatra 好的,我明白了。关于你的回答,我有一个问题:它依赖于双向绑定,这是我一直在努力解决的问题,因为我无法完全理解什么确切地提供/不提供足够强的引用。我总有一天会解决这个问题,但与此同时,你回答底部的双向绑定是否安全免受垃圾回收?希望你明白我的意思。再次感谢。 - pateksan
1
@pateksan 简短(也是最后一个)答案:是的 - 抱歉,这已经太过离题了 - 要学习有关引用的所有知识,请通过与弱/强引用相关的Java基础教程,并将所学应用于此上下文(例如,通过思考此处的引用路径,编写测试,遇到困难时提出问题.. :) 玩得开心! - kleopatra

33

@kleopatra的方向是正确的,但复制粘贴的解决方案感觉很尴尬,而基于TextFormatter的解决方案对我完全没有用。因此,这里有一个更短的解决方案,它强制Spinner调用其私有的commitEditorText()方法:

spinner.focusedProperty().addListener((observable, oldValue, newValue) -> {
  if (!newValue) {
    spinner.increment(0); // won't change value, but will commit editor
  }
});

谢谢!非常好用。 - AvaLanCS
很好的答案。有效。 - Roger
1
@kleopatra和其他人:如果这是一个如此简短而甜美的解决方案,为什么它仍然没有超过kleopatra答案的分数?这两个答案现在几乎已经有5年的历史了。这个答案是否存在用户需要注意的问题?还是人们只是没有向下滚动足够远? - pateksan

4

这是控件的标准行为,根据文档:

可编辑属性用于指定是否可以在Spinner编辑器中输入用户输入。如果editable为true,则一旦用户键入并按下Enter键,将接收用户输入。此时,将输入传递给SpinnerValueFactory转换器StringConverter.fromString(String)方法。从此调用返回的值(类型为T)然后发送到SpinnerValueFactory.setValue(Object)方法。如果该值有效,它将保持作为值。如果无效,则值工厂需要相应地做出反应并撤销此更改。

您可以使用键盘事件来监听并随时调用控件的编辑提交方法。


你可以尝试这个... spinner.getEditor().textProperty().addListener((observable, oldValue, newValue) -> { commitEditorText(); }); - purring pigeon

3
这里是对Sergio解决方案的改进版本。
initialize方法将把Sergio的代码附加到控制器中的所有Spinner组件。
public void initialize(URL location, ResourceBundle resources) {
    for (Field field : getClass().getDeclaredFields()) {
        try {
            Object obj = field.get(this);
            if (obj != null && obj instanceof Spinner)
                ((Spinner) obj).focusedProperty().addListener((observable, oldValue, newValue) -> {
                    if (!newValue) {
                        ((Spinner) obj).increment(0);
                    }
                });
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

0

我使用一种替代方法 - 在打字时实时更新。这是我的当前实现:

getEditor().textProperty().addListener { _, _, nv ->
    // let the user clear the field without complaining
    if(nv.isNotEmpty()) {
        Double newValue = getValue()
        try {
            newValue = getValueFactory().getConverter().fromString(nv)
        } catch (Exception e) { /* user typed an illegal character */ } 
        getValueFactory().setValue(newValue)
    }

0

使用监听器应该可以解决问题。您可以通过下拉列表的编辑器访问输入的值:

spinner.getEditor().getText();

也许你忘记了解释如何做到这一点? - Mateus Viccari
原始问题展示了如何添加监听器,OP 不确定的部分是“获取输入值的方法”,这在我的回答中有所展示。 - Amber

0

我使用了这种方法

public class SpinnerFocusListener<T> implements ChangeListener<Boolean> {
   private Spinner<T> spinner;

   public SpinnerFocusListener(Spinner<T> spinner) {
       super();
       this.spinner = spinner;      
       this.spinner.getEditor().focusedProperty().addListener(this);
   }

   @Override
   public void changed(ObservableValue<? extends Boolean> observable, 
                           Boolean oldValue, Boolean newValue) {        
        StringConverter<T>converter=spinner.getValueFactory().getConverter();
        TextField editor=spinner.getEditor();
        String text=editor.getText();
        try {
            T value=converter.fromString(text);
            spinner.getValueFactory().setValue(value);
        }catch(Throwable ex) {
            editor.setText(converter.toString(spinner.getValue()));
        }   
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接