TreeView/TreeTableView - KeyEvent F2 导致 JavaFX 内部 NPE

TreeView/TreeTableView - KeyEvent F2 causes JavaFX internal NPE

通过这样做将 "standard" 编辑器放在 TreeTableView 的第 0 列上:

treeTableView.columns.get(0).setCellFactory( TextFieldTreeTableCell.forTreeTableColumn());

...很高兴发现有几件事可以开始编辑 TreeItem:一个是单击单元格,另一个是按 F2。

然而,令我略感沮丧的是,如果我启动该应用程序,并且没有使用鼠标选择 TreeItem,而是以编程方式选择了根 TreeItem 的第一个子节点,如果然后我(可能仅在使用键盘键导航之后)按 F2,JavaFX 内部抛出一个 NPE,它看起来像这样:

java.lang.NullPointerException: null
    at com.sun.javafx.scene.control.behavior.TableViewBehaviorBase.activate(TableViewBehaviorBase.java:890)
    at com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
    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.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:4058)
    at javafx.scene.Scene$KeyHandler.access00(Scene.java:4004)
    at javafx.scene.Scene.processKeyEvent(Scene.java:2121)
    at javafx.scene.Scene$ScenePeerListener.keyEvent(Scene.java:2595)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:217)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler$KeyEventNotification.run(GlassViewEventHandler.java:149)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleKeyEvent(GlassViewEventHandler.java:248)
    at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:390)
    at com.sun.javafx.tk.quantum.GlassViewEventHandler.handleKeyEvent(GlassViewEventHandler.java:247)
    at com.sun.glass.ui.View.handleKeyEvent(View.java:547)
    at com.sun.glass.ui.View.notifyKey(View.java:971)
    at com.sun.glass.ui.gtk.GtkApplication._runLoop(Native Method)
    at com.sun.glass.ui.gtk.GtkApplication.lambda$runLoop(GtkApplication.java:277)

不可能鼠标直接把焦点放在TreeItem上,因为TreeItem的超class是Object。我在 Scene 上放置了一个焦点所有者侦听器,它确认当您单击 TreeTableView 时没有任何与焦点相关的变化。然而,从质量上讲,确实发生了一些变化。

可能与 BehaviorInputMap、"skins" 或我不知道的许多神秘的 JavaFX 事物中的任何一个有关。

在这种情况下,在我看来,一种选择可能是以某种方式拦截 F2 键击,并阻止默认功能,并以编程方式发起对 TreeItem 的编辑,该 TreeItem 已被选中但未选中'没有焦点。

注意我在 TreeTableView 上放置了 "key pressed" 和 "key released" 事件监听器。其中包括以下行:

TreeTablePosition pos = treeTableView.getFocusModel().getFocusedCell();

我正在描述的 "anomalous" 事件,其中 JavaFX 内部抛出 NPE,其特征在于,在 "key release" 侦听器中,pos.col == -1pos.getTableColumn() == null。我还可以从日志记录中看出,NPE 发生在 "key released" 处理程序响应之前。

有关信息,当我按下 F2 而没有先单击鼠标 TreeItem 时,"key pressed" 侦听器从不记录任何内容(毫无疑问,JavaFX 内部 Exception 终止了正常 "broadcast" 听众),让我假设 JavaFX 框架也先于任何用户添加的按键监听器。

找到答案(或答案)。实际上 TreeTableView 内部有一个焦点系统...参见 getFocusModel()Stage 的焦点所有者机制似乎没有发现这方面的变化:

stage.getScene().focusOwnerProperty().addListener( new ChangeListener(){
...

所以你要做的就是在select离子改变时改变内部焦点模型:

treeTableView.getSelectionModel().getSelectedItems().addListener(new ListChangeListener() {
    void onChanged(ListChangeListener.Change c) {
        TreeItem newSelectedItem = c.list.get(0);
        int row = treeTableView.getRow( newSelectedItem );
        treeTableView.getFocusModel().focus( row, treeTableView.getColumns().get(0) );
    }
}

关于这一点需要说明一两点:

  1. 我不完全确定 c.list.get( 0 ) 总能保证交付新的 selected 项目...如果 selection 模式是多重的,则尤其如此select离子
  2. 我有点惊讶地发现,即使您单击 table 列之一(即第 1、2、3 列...)中的单元格,而不是树列 (第 0 列),您似乎总是从第 0 列返回 TreeItem。在正常的 table 中,您当然可以 select 随机放置一个单元格块。我现在想知道您是否可以使用此控件执行此操作,或者所有 selection 是否真的是 "row selection".
  3. 我本以为上述功能应该包含在class的默认实现中:如果讨厌使用或不能使用的人进行简单的键盘操作肯定不是很好鼠标引发 JavaFX 异常(也意味着编辑操作不会发生)。

如果您 select 您的手机使用 TableSelectionModel#select(int,TableColumnBase) 那么 NPE 就不会发生。

Selects the cell at the given row/column intersection. If the table control is in its 'cell selection' mode (where individual cells can be selected, rather than entire rows), and if the column argument is null, this method should select all cells in the given row.

例如:

TreeTableColumn col = treeTableView.getColumns().get(0);
treeTableView.getSelectionModel().select(rowIndex, col);

有了上面的这些你就不用担心焦点模型了。这样做的原因是因为默认的 selection 模型实现将为您关注给定的行+列(即单元格)(不确定这是否是有保证的行为,但它肯定是礼貌的行为)。

基本上,您当前的代码导致一行成为 selected/focused 但不是该行中的特定单元格。因此,当您尝试输入 "edit mode" 时,无法确定要定位到哪个单元格。我认为 NPE 是一个错误。不确定当没有列是 selected/focused 时 意味着 会发生什么,但是控件的行为应该优雅地处理这种情况。

如果有兴趣,下面是我的调试过程。


这是负责 NPE 的代码:

protected void activate(KeyEvent e) {
    TableSelectionModel sm = getSelectionModel();
    if (sm == null) return;

    TableFocusModel fm = getFocusModel();
    if (fm == null) return;

    TablePositionBase<TC> cell = getFocusedCell();
    sm.select(cell.getRow(), cell.getTableColumn());
    setAnchor(cell);

    // check if we are editable
    boolean isEditable = isControlEditable() && cell.getTableColumn().isEditable();

    // edit this row also
    if (isEditable && cell.getRow() >= 0) {
        editCell(cell.getRow(), cell.getTableColumn());
        e.consume();
    }
}

具体来说,这是有问题的行:

boolean isEditable = isControlEditable() && cell.getTableColumn().isEditable();

这意味着 cell 为 null 或 getTableColumn() 返回 null。在 JEP 358: Helpful NullPointerExceptions 的帮助下进行一些调试后,我们知道是后者:

Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "javafx.scene.control.TableColumnBase.isEditable()" because the return value of "javafx.scene.control.TablePositionBase.getTableColumn()" is null
    at javafx.controls/com.sun.javafx.scene.control.behavior.TableViewBehaviorBase.activate(TableViewBehaviorBase.java:898)

如果 getTableColumn() 返回 null 那么这意味着,就焦点模型而言,没有焦点列,因此没有特定单元格焦点。解决方法是 select/focus 特定的行+列(即单元格),而不仅仅是整行。

给出的答案只涵盖了问题的一部分:例如如果您在最后一列后面单击鼠标并按 F2,则 startEdit 仍会抛出 NPE。

我在我的项目中通过安装一个焦点侦听器解决了这个问题,只要没有有效的列(无论出于何种原因),它就会选择默认列:

//guarantee that always a valid column is selected to avoid NPE during startEdit
tableView.getFocusModel().focusedCellProperty().addListener((observable, oldValue, newValue) -> {
  if (newValue.getTableColumn() == null){
    tableView.getSelectionModel().select(newValue.getRow(), defaultColumn);
  }
});

例如如果 defaultColumn 应该是您可以设置的第一列

defaultColumn = tableView.getColumns.get(0)