15

是否有任何简单的方法可以让 TreeTableView(或 TableView)尝试在失去焦点时提交值?

不幸的是,我没有成功使用 javafx TableCellFactories 的任何默认实现,这就是为什么我尝试了自己的 TreeTableCell 实现以及一些不同的 tableCell 实现,比如Graham Smith的那个,这似乎是最直接的,因为它已经实现了一个钩子因为焦点丢失,但该值从未提交,并且用户更改被重置为原始值。

我的猜测是,每当失去焦点时,受影响的 Cell 的 editingProperty 总是已经是 false,这导致 Cell 永远不会在 focusLost 上提交值。这里是原始(oracle-)TreeTableCell 实现(8u20ea)的相关部分,这导致我的方法失败:

 @Override public void commitEdit(T newValue) {
        if (! isEditing()) return; // <-- here my approaches are blocked, because on focus lost its not editing anymore.

        final TreeTableView<S> table = getTreeTableView();
        if (table != null) {
            @SuppressWarnings("unchecked")
            TreeTablePosition<S,T> editingCell = (TreeTablePosition<S,T>) table.getEditingCell();

            // Inform the TableView of the edit being ready to be committed.
            CellEditEvent<S,T> editEvent = new CellEditEvent<S,T>(
                table,
                editingCell,
                TreeTableColumn.<S,T>editCommitEvent(),
                newValue
            );

            Event.fireEvent(getTableColumn(), editEvent);
        }

        // inform parent classes of the commit, so that they can switch us
        // out of the editing state.
        // This MUST come before the updateItem call below, otherwise it will
        // call cancelEdit(), resulting in both commit and cancel events being
        // fired (as identified in RT-29650)
        super.commitEdit(newValue);

        // update the item within this cell, so that it represents the new value
        updateItem(newValue, false);

        if (table != null) {
            // reset the editing cell on the TableView
            table.edit(-1, null);

            // request focus back onto the table, only if the current focus
            // owner has the table as a parent (otherwise the user might have
            // clicked out of the table entirely and given focus to something else.
            // It would be rude of us to request it back again.
            ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(table);
        }
    }

在调用原始 commitEdit() 方法之前,我成功地覆盖了此方法并“手动”提交了值,但这会导致像 enter 这样的键提交两次提交值(在键 + 焦点丢失时)。此外,我根本不喜欢我的方法,所以我想知道,是否有其他人以“更好”的方式解决了这个问题?

4

7 回答 7

12

经过一番挖掘,结果发现罪魁祸首(又名:在 textField 失去焦点之前取消编辑的协作者)是 TableCellBehaviour/Base 在处理鼠标按下时:

  • mousePressed 调用simpleSelect(..)
  • 在检测到单击时它调用edit(-1, null)
  • 在 TableView 上调用相同的方法
  • 它将它的 editingCell 属性设置为 null
  • tableCell 监听该属性并通过取消自己的编辑来做出反应

不幸的是,hackaround 需要 3 个协作者

  • 带有附加 api 的 TableView 以终止编辑
  • simpleSelect(...)在调用 super 之前调用附加 api(而不是 edit(-1..))的重​​写的 TableCellBehaviour
  • 配置有扩展行为知道表的扩展属性的 TableCell

一些代码片段(完整代码):

// on XTableView:
public void terminateEdit() {
    if (!isEditing()) return;
    // terminatingCell is a property that supporting TableCells can listen to
    setTerminatingCell(getEditingCell());
    if (isEditing()) throw new IllegalStateException(
          "expected editing to be terminated but was " + getEditingCell());
    setTerminatingCell(null);
}

// on XTableCellBehaviour: override simpleSelect
@Override
protected void simpleSelect(MouseEvent e) {
    TableCell<S, T> cell = getControl();
    TableView<S> table = cell.getTableColumn().getTableView();
    if (table instanceof XTableView) {
        ((XTableView<S>) table).terminateEdit();
    }
    super.simpleSelect(e);
}

// on XTextFieldTableCell - this method is called from listener
// to table's terminatingCell property
protected void terminateEdit(TablePosition<S, ?> newPosition) {
    if (!isEditing() || !match(newPosition)) return;
    commitEdit();
}

protected void commitEdit() {
    T edited = getConverter().fromString(myTextField.getText());
    commitEdit(edited);
}

/**
 * Implemented to create XTableCellSkin which supports terminating edits.
 */
@Override
protected Skin<?> createDefaultSkin() {
    return new XTableCellSkin<S, T>(this);
}

注意:TableCellBehaviour 的实现在 jdk8u5 和 jdk8u20 之间发生了巨大变化(黑客的乐趣 - 不适合生产使用 ;-) - 在后者中覆盖的方法是handleClicks(..)

顺便说一句:对JDK-8089514(旧 jira 中的 RT-18492)的大规模投票可能会加速核心修复。不幸的是,至少需要作者角色来投票/评论新跟踪器中的错误。

于 2014-08-13T16:12:16.767 回答
5

我也需要这个功能并做了一些研究。我遇到了上面提到的 XTableView 黑客攻击的一些稳定性问题。

由于问题似乎是焦点丢失时 commitEdit() 不会生效,为什么你不只是从 TableCell 调用你自己的提交回调,如下所示:

public class SimpleEditingTextTableCell extends TableCell {
    private TextArea textArea;
    Callback commitChange;

    public SimpleEditingTextTableCell(Callback commitChange) {
        this.commitChange = commitChange;
    }

    @Override
    public void startEdit() {
         ...

        getTextArea().focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
                if (!arg2) {
                    //commitEdit is replaced with own callback
                    //commitEdit(getTextArea().getText());

                    //Update item now since otherwise, it won't get refreshed
                    setItem(getTextArea().getText());
                    //Example, provide TableRow and index to get Object of TableView in callback implementation
                    commitChange.call(new TableCellChangeInfo(getTableRow(), getTableRow().getIndex(), getTextArea().getText()));
                }
            }
        });
       ...
    }
    ...
}

在单元工厂中,您只需将提交的值存储到对象或执行任何必要的操作以使其永久化:

col.setCellFactory(new Callback<TableColumn<Object, String>, TableCell<Object, String>>() {
            @Override
            public TableCell<Object, String> call(TableColumn<Object, String> p) {
                return new SimpleEditingTextTableCell(cellChange -> {
                            TableCellChangeInfo changeInfo = (TableCellChangeInfo)cellChange;
                            Object obj = myTableView.getItems().get(changeInfo.getRowIndex());
                            //Save committed value to the object in tableview (and maybe to DB)
                            obj.field = changeInfo.getChangedObj().toString();
                            return true;
                        });
            }
        });

到目前为止,我还没有发现这个解决方法有任何问题。另一方面,我还没有对此进行过广泛的测试。

编辑:好吧,经过一些测试后发现,该解决方法在 tableview 中的大数据上运行良好,但是在失去焦点后空的 tableview 单元格没有得到更新,只有在再次双击它时才会更新。有办法刷新表格视图,但对我来说太多的黑客行为......

EDIT2:添加了 setItem(getTextArea().getText()); 在调用回调之前 -> 也适用于空表视图。

于 2014-12-08T19:07:21.467 回答
1

我更喜欢在现有代码上尽可能多地构建,因为这种行为仍然没有通过 Java 10 修复,这里有一个基于J. Duke 的bug 解决方案的更通用的方法:JDK-8089311

public class TextFieldTableCellAutoCmt<S, T> extends TextFieldTableCell<S, T> {

    protected TextField txtFldRef;
    protected boolean isEdit;

    public TextFieldTableCellAutoCmt() {
        this(null);
    }

    public TextFieldTableCellAutoCmt(final StringConverter<T> conv) {
        super(conv);
    }

    public static <S> Callback<TableColumn<S, String>, TableCell<S, String>> forTableColumn() {
        return forTableColumn(new DefaultStringConverter());
    }

    public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(final StringConverter<T> conv) {
        return list -> new TextFieldTableCellAutoCmt<S, T>(conv);
    }

    @Override
    public void startEdit() {
        super.startEdit();
        isEdit = true;
        if (updTxtFldRef()) {
            txtFldRef.focusedProperty().addListener(this::onFocusChg);
            txtFldRef.setOnKeyPressed(this::onKeyPrs);
        }
    }

    /**
     * @return whether {@link #txtFldRef} has been changed
     */
    protected boolean updTxtFldRef() {
        final Node g = getGraphic();
        final boolean isUpd = g != null && txtFldRef != g;
        if (isUpd) {
            txtFldRef = g instanceof TextField ? (TextField) g : null;
        }
        return isUpd;
    }

    @Override
    public void commitEdit(final T valNew) {
        if (isEditing()) {
            super.commitEdit(valNew);
        } else {
            final TableView<S> tbl = getTableView();
            if (tbl != null) {
                final TablePosition<S, T> pos = new TablePosition<>(tbl, getTableRow().getIndex(), getTableColumn()); // instead of tbl.getEditingCell()
                final CellEditEvent<S, T> ev  = new CellEditEvent<>(tbl, pos, TableColumn.editCommitEvent(), valNew);
                Event.fireEvent(getTableColumn(), ev);
            }
            updateItem(valNew, false);
            if (tbl != null) {
                tbl.edit(-1, null);
            }
            // TODO ControlUtils.requestFocusOnControlOnlyIfCurrentFocusOwnerIsChild(tbl);
        }
    }

    public void onFocusChg(final ObservableValue<? extends Boolean> obs, final boolean v0, final boolean v1) {
        if (isEdit && !v1) {
            commitEdit(getConverter().fromString(txtFldRef.getText()));
        }
    }

    protected void onKeyPrs(final KeyEvent e) {
        switch (e.getCode()) {
        case ESCAPE:
            isEdit = false;
            cancelEdit(); // see CellUtils#createTextField(...)
            e.consume();
            break;
        case TAB:
            if (e.isShiftDown()) {
                getTableView().getSelectionModel().selectPrevious();
            } else {
                getTableView().getSelectionModel().selectNext();
            }
            e.consume();
            break;
        case UP:
            getTableView().getSelectionModel().selectAboveCell();
            e.consume();
            break;
        case DOWN:
            getTableView().getSelectionModel().selectBelowCell();
            e.consume();
            break;
        default:
            break;
        }
    }
}
于 2018-05-10T14:44:21.753 回答
1

保留这是一个愚蠢的建议。似乎太容易了。但是,为什么不在TableCell#cancelEdit()调用时手动覆盖并保存这些值呢?当单元格失去焦点时,cancelEdit()总是调用它来取消编辑。

class EditableCell extends TableCell<ObservableList<StringProperty>, String> {

    private TextField textfield = new TextField();
    private int colIndex;
    private String originalValue = null;

    public EditableCell(int colIndex) {
        this.colIndex = colIndex;
        textfield.prefHeightProperty().bind(heightProperty().subtract(2.0d));
        this.setPadding(new Insets(0));
        this.setAlignment(Pos.CENTER);

        textfield.setOnAction(e -> {
            cancelEdit();
        });

        textfield.setOnKeyPressed(e -> {
            if (e.getCode().equals(KeyCode.ESCAPE)) {
                textfield.setText(originalValue);
            }
        });
    }

    @Override
    public void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (isEmpty()) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                textfield.setText(item);
                setGraphic(textfield);
                setText(null);
            } else {
                setText(item);
                setGraphic(null);
            }
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        originalValue = getItem();
        textfield.setText(getItem());
        setGraphic(textfield);
        setText(null);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setGraphic(null);
        setText(textfield.getText());
        ObservableList<StringProperty> row = getTableView().getItems().get(getIndex());
        row.get(colIndex).set(getText());
    }
}

我不知道。也许我错过了一些东西。但这似乎对我有用。

更新:添加了取消编辑功能。您现在可以通过在聚焦文本字段时按 Escape 来取消编辑。还添加了以便您可以通过在聚焦文本字段时按 Enter 来保存编辑。

于 2015-10-29T09:43:22.103 回答
0

由于TextFieldTableCell缺少功能(如JDK 错误跟踪器中所估计),替代解决方案可能会起作用。忘记TextFieldTableCell并使用其中包含的自定义TableCellTextField。习俗TableCell

public class CommentCell extends TableCell<ListItem, String> {

    private final TextField comment = new TextField();

    public CommentCell() {
        this.comment.setMaxWidth( Integer.MAX_VALUE );
        this.comment.setDisable( true );
        this.comment.focusedProperty().addListener( new ChangeListener<Boolean>() {
            @Override
            public void changed( ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue,
                    Boolean newPropertyValue ) {
                if ( !newPropertyValue ) {
                    // Binding the TextField text to the model
                    MainController.getInstance().setComment( getTableRow().getIndex(), comment.getText() );
                }
            }
        } );
        this.setGraphic( this.comment );
    }

    @Override
    protected void updateItem( String s, boolean empty ) {
        // Checking if the TextField should be editable (based on model condition)
        if ( MainController.getInstance().isDependency( getTableRow().getIndex() ) ) {
            this.comment.setDisable( false );
            this.comment.setEditable( true );
        }
        // Setting the model value as the text for the TextField
        if ( s != null && !s.isEmpty() ) {
            this.comment.setText( s );
        }
    }
}

UI 显示可能与 a 不同,TextFieldTableCell但至少它允许更好的可用性:

界面显示

于 2016-03-17T09:17:23.370 回答
0

我找到了一个简单的解决方案,只需要为特定于数据类型的列提供提交函数:

TableColumn msgstr = new TableColumn("msgstr");
msgstr.setMinWidth(100);
msgstr.prefWidthProperty().bind(widthProperty().divide(3));
msgstr.setCellValueFactory(
        new PropertyValueFactory<>("msgstr")
);
msgstr.setOnEditCommit(new EventHandler<CellEditEvent<PoEntry, String>>() {
@Override
public void handle(CellEditEvent<PoEntry, String> t) {
    ((PoEntry)t.getTableView().getItems().get(t.getTablePosition().getRow())).setMsgstr(t.getNewValue());
}
});
于 2016-07-29T17:05:05.980 回答
0

你需要:

  • 一个CellEditor;
  • 一个TableCellTreeCell子类;和
  • 细胞工厂方法。

有关下面显示的类的更多详细信息,请参阅:

单元格编辑器

CellEditor负责处理EscandEnter以及焦点丢失:

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.input.KeyEvent;

import java.util.function.Consumer;

import static javafx.application.Platform.runLater;
import static javafx.scene.input.KeyCode.ENTER;
import static javafx.scene.input.KeyCode.TAB;
import static javafx.scene.input.KeyEvent.KEY_RELEASED;

public class CellEditor {
  private FocusListener mFocusListener;
  private final Property<String> mInputText = new SimpleStringProperty();
  private final Consumer<String> mConsumer;

  /**
   * Responsible for accepting the text when users press the Enter or Tab key.
   */
  private class KeyHandler implements EventHandler<KeyEvent> {
    @Override
    public void handle( final KeyEvent event ) {
      if( event.getCode() == ENTER || event.getCode() == TAB ) {
        commitEdit();
        event.consume();
      }
    }
  }

  /**
   * Responsible for committing edits when focus is lost. This will also
   * deselect the input field when focus is gained so that typing text won't
   * overwrite the entire existing text.
   */
  private class FocusListener implements ChangeListener<Boolean> {
    private final TextField mInput;

    private FocusListener( final TextField input ) {
      mInput = input;
    }

    @Override
    public void changed(
      final ObservableValue<? extends Boolean> c,
      final Boolean endedFocus, final Boolean beganFocus ) {

      if( beganFocus ) {
        runLater( mInput::deselect );
      }
      else if( endedFocus ) {
        commitEdit();
      }
    }
  }

  /**
   * Generalized cell editor suitable for use with {@link TableCell} or
   * {@link TreeCell} instances.
   *
   * @param consumer        Converts the field input text to the required
   *                        data type.
   * @param graphicProperty Defines the graphical user input field.
   */
  public CellEditor(
    final Consumer<String> consumer,
    final ObjectProperty<Node> graphicProperty ) {
    assert consumer != null;
    mConsumer = consumer;

    init( graphicProperty );
  }

  private void init( final ObjectProperty<Node> graphicProperty ) {
    final var keyHandler = new KeyHandler();

    // When the text field is added as the graphics context, we hook into
    // the changed value to get a handle on the text field. From there it is
    // possible to add change the keyboard and focus behaviours.
    graphicProperty.addListener( ( c, o, n ) -> {
      if( o instanceof TextField ) {
        o.removeEventHandler( KEY_RELEASED, keyHandler );
        o.focusedProperty().removeListener( mFocusListener );
      }

      if( n instanceof final TextField input ) {
        n.addEventFilter( KEY_RELEASED, keyHandler );
        mInputText.bind( input.textProperty() );
        mFocusListener = new FocusListener( input );
        n.focusedProperty().addListener( mFocusListener );
      }
    } );
  }

  private void commitEdit() {
    mConsumer.accept( mInputText.getValue() );
  }
}

替代表单元格

AltTableCellan和 an之间的唯一区别AltTreeCell是继承层次结构。它们在其他方面是相同的:

public class AltTableCell<S, T> extends TextFieldTableCell<S, T> {
  public AltTableCell( final StringConverter<T> converter ) {
    super( converter );

    assert converter != null;

    new CellEditor(
      input -> commitEdit( getConverter().fromString( input ) ),
      graphicProperty()
    );
  }
}

具体来说,AltTreeCell将开始:

public class AltTreeCell<T> extends TextFieldTreeCell<T>

细胞工厂方法

将备用表格单元格分配给表格列的单元格工厂:

final var column = new TableColumn<Entry<K, V>, T>( label );

column.setEditable( true );
column.setCellFactory(
  tableColumn -> new AltTableCell<>(
    new StringConverter<>() {
      @Override
      public String toString( final T object ) {
        return object.toString();
      }

      @Override
      public T fromString( final String string ) {
        return (T) string;
      }
    }
  )
);

对于树细胞,它非常相似:

final var view = new TreeView<>(); // ...

view.setEditable( true );
view.setCellFactory( treeView -> new AltTreeCell<>( converter ) );
于 2021-12-21T06:40:30.883 回答