【问题标题】:How to use arrow buttons to traverse cells in edit mode in TableViewTableView中如何使用箭头按钮在编辑模式下遍历单元格
【发布时间】:2019-08-15 06:13:21
【问题描述】:

我想使用箭头/输入键来遍历TableView 中的单元格,但是,如果我尝试在我的自定义 EditCell 类中实现它,它似乎不起作用。有没有办法做到这一点?我在TextField 上尝试了一个监听器,但它实际上并没有在实际单元格中启动焦点。

这是我的代码:

Tester.java

package tester;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Tester extends Application
{

    @Override
    public void start(Stage primaryStage)
    {

        TableView<LineItem> table = new TableView<>();

        Callback<TableColumn<LineItem, String>, TableCell<LineItem, String>> textFactoryEditable = (TableColumn<LineItem, String> p) -> new EditableTextCell();

        TableColumn<LineItem, String> column1 = new TableColumn<>("Test1");
        column1.setCellValueFactory(cellData -> cellData.getValue().getString1Property());
        column1.setEditable(true);
        column1.setCellFactory(textFactoryEditable);

        table.getColumns().add(column1);

        TableColumn<LineItem, String> column2 = new TableColumn<>("Test2");
        column2.setCellValueFactory(cellData -> cellData.getValue().getString2Property());
        column2.setEditable(true);
        column2.setCellFactory(textFactoryEditable);

        table.getColumns().add(column2);

        table.getItems().add(new LineItem());
        table.getItems().add(new LineItem());
        table.getItems().add(new LineItem());

        table.setPrefWidth(500);

        HBox root = new HBox();
        root.getChildren().addAll(table);

        Scene scene = new Scene(root, 500, 500);

        primaryStage.setTitle("Hello World!");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args)
    {
        launch(args);
    }

}

LineItem.java

package tester;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class LineItem
{

    private final StringProperty string1;
    private final StringProperty string2;

    public LineItem()
    {
        this.string1 = new SimpleStringProperty();
        this.string2 = new SimpleStringProperty();
    }

    public final StringProperty getString1Property()
    {
        return this.string1;
    }

    public final StringProperty getString2Property()
    {
        return this.string2;
    }
}

EditableTextCell.java

package tester;

import java.util.Objects;
import javafx.beans.value.ObservableValue;
import javafx.beans.value.WritableValue;
import javafx.geometry.Pos;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;

public class EditableTextCell<E> extends TableCell<E, String>
{

    private final TextField textField;
    private boolean updating = false;

    public EditableTextCell()
    {
        textField = new TextField();
        textField.setAlignment(Pos.CENTER_RIGHT);


        textField.textProperty().addListener((ObservableValue<? extends String> o, String oldValue, String newValue) ->
        {

            if (!updating)
            {
                ((WritableValue<String>) getTableColumn().getCellObservableValue((E) getTableRow().getItem())).setValue(newValue);
                getTableView().scrollTo(getTableRow().getIndex());
                getTableView().scrollToColumn(getTableColumn());
            }

        });

        textField.setOnKeyPressed((KeyEvent ke) ->
        {
            switch (ke.getCode())
            {
                case DOWN:
                    getTableView().getFocusModel().focusBelowCell();
                    break;
                case UP:
                    getTableView().getFocusModel().focusAboveCell();
                    break;
                case RIGHT:
                    getTableView().getFocusModel().focusRightCell();
                    break;
                case LEFT:
                    getTableView().getFocusModel().focusLeftCell();
                    break;
                default:
                    break;
            }
        });

    }

    @Override
    protected void updateItem(String item, boolean empty)
    {
        super.updateItem(item, empty);
        if (empty)
        {
            setGraphic(null);
        } else
        {
            setGraphic(textField);
            if (!Objects.equals(textField.getText(), item))
            {
                // prevent own updates from moving the cursor
                updating = true;
                textField.setText(item);
                updating = false;

            }
        }
    }
}

【问题讨论】:

  • 尚未深入分析您的代码,但我注意到enable cell selection 似乎没有您。默认情况下,TableView 处于“行选择”模式。
  • 谢谢,成功了。现在我能够在 FocusProperty 上添加一个监听器,你能告诉我这是否是最好的方法吗?它似乎正在工作......
  • @kleopatra TableView 应该支持“始终编辑”单元格,根据其文档:“您可以选择单元格是否永久处于编辑状态 [...] ,或在编辑开始时切换到不同的 UI [...]"。不幸的是,使用普通的编辑 API 似乎无法做到这一点——至少不简单。如果你看一下CheckBoxTableCell 的实现,你会发现它的作用类似于 OP 的作用。
  • @Slaw 好点 :) 并且有趣(阅读:从未真正见过它),尽管在 tableView 和 checkBoxTableCell 的文档中没有完全指定(或有些矛盾)在编辑中永久状态 vs. 不需要进入编辑状态 ...
  • 不相关:您没有遵循 LineItem 中的 fx 命名约定 - 属性访问器应该是 Property(与 getProperty)

标签: java javafx


【解决方案1】:

行选择模式

尽管有my comment,但您似乎不需要为此启用cell selection。从CheckBoxTableCell 的实现中获得灵感,您的自定义TableCell 应该采用某种形式的回调来获取模型属性;它还可能需要StringConverter,允许您将TableCellStrings 一起使用。这是一个例子:

import java.util.Objects;
import java.util.function.IntFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView.TableViewFocusModel;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;

public class CustomTableCell<S, T> extends TableCell<S, T> {

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

    public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(
            IntFunction<Property<T>> extractor, StringConverter<T> converter) {
        Objects.requireNonNull(extractor);
        Objects.requireNonNull(converter);
        return column -> new CustomTableCell<>(extractor, converter);
    }

    private final ObjectProperty<IntFunction<Property<T>>> extractor = new SimpleObjectProperty<>(this, "extractor");
    public final void setExtractor(IntFunction<Property<T>> callback) { extractor.set(callback); }
    public final IntFunction<Property<T>> getExtractor() { return extractor.get(); }
    public final ObjectProperty<IntFunction<Property<T>>> extractorProperty() { return extractor; }

    private final ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
    public final void setConverter(StringConverter<T> converter) { this.converter.set(converter); }
    public final StringConverter<T> getConverter() { return converter.get(); }
    public final ObjectProperty<StringConverter<T>> converterProperty() { return converter; }

    private Property<T> property;
    private TextField textField;

    public CustomTableCell(IntFunction<Property<T>> extractor, StringConverter<T> converter) {
        setExtractor(extractor);
        setConverter(converter);

        // Assumes this TableCell will never become part of a different TableView
        // after the first one. Also assumes the focus model of the TableView will
        // never change. These are not great assumptions (especially the latter),
        // but this is only an example.
        tableViewProperty().addListener((obs, oldTable, newTable) ->
                newTable.getFocusModel().focusedCellProperty().addListener((obs2, oldPos, newPos) -> {
                    if (getIndex() == newPos.getRow() && getTableColumn() == newPos.getTableColumn()) {
                        textField.requestFocus();
                    }
                })
        );
    }

    @Override
    protected void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
            cleanUpProperty();
        } else {
            initializeTextField();
            cleanUpProperty();

            property = getExtractor().apply(getIndex());
            Bindings.bindBidirectional(textField.textProperty(), property, getConverter());

            setGraphic(textField);
            if (getTableView().getFocusModel().isFocused(getIndex(), getTableColumn())) {
                textField.requestFocus();
            }
        }
    }

    private void cleanUpProperty() {
        if (property != null) {
            Bindings.unbindBidirectional(textField.textProperty(), property);
            property = null;
        }
    }

    private void initializeTextField() {
        if (textField == null) {
            textField = new TextField();
            textField.addEventFilter(KeyEvent.KEY_PRESSED, this::processArrowKeys);
            textField.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
                if (isFocused) {
                    getTableView().getFocusModel().focus(getIndex(), getTableColumn());
                }
            });
        }
    }

    private void processArrowKeys(KeyEvent event) {
        if (event.getCode().isArrowKey()) {
            event.consume();

            TableViewFocusModel<S> model = getTableView().getFocusModel();
            switch (event.getCode()) {
                case UP:
                    model.focusAboveCell();
                    break;
                case RIGHT:
                    model.focusRightCell();
                    break;
                case DOWN:
                    model.focusBelowCell();
                    break;
                case LEFT:
                    model.focusLeftCell();
                    break;
                default:
                    throw new AssertionError(event.getCode().name());
            }
            getTableView().scrollTo(model.getFocusedCell().getRow());
            getTableView().scrollToColumnIndex(model.getFocusedCell().getColumn());
        }
    }

}

该示例并非详尽无遗,并且做出了无法保证的假设,但这只是一个示例,因此我将任何调整留给您。其中一项改进可能是以某种方式包含TextFormatter。也就是说,我相信它提供了您正在寻找的基本功能。

要使用此单元格,您只需设置每个TableColumncellFactory。没有必要设置cellValueFactory,这样做实际上可能是有害的,这取决于updateItem 的调用方式。基本上,它看起来像:

TableView<YourModel> table = ...;

TableColumn<YourModel, String> column = new TableColumn<>("Column");
column.setCellFactory(CustomTableCell.forTableColumn(i -> table.getItems().get(i).someProperty()));
table.getColumns().add(column);

单元格选择模式

但是,您尝试实现的这种行为似乎本质上是基于单元格的,因此启用单元格选择可能会更好。这允许自定义TableCell 将其行为基于选择而不是焦点,并将箭头键处理留给TableView。以下是对上述示例稍作修改的版本:

import java.util.Objects;
import java.util.function.IntFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventDispatcher;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;

public class CustomTableCell<S, T> extends TableCell<S, T> {

    /* 
     * -- CODE OMITTED --
     *
     * The factory methods (forTableColumn) and properties (extractor
     * and converter) have been omitted for brevity. They are defined
     * and used exactly the same way as in the previous example.
     */

    private Property<T> property;
    private TextField textField;

    public CustomTableCell(IntFunction<Property<T>> extractor, StringConverter<T> converter) {
        setExtractor(extractor);
        setConverter(converter);
    }

    @Override
    public void updateSelected(boolean selected) {
        super.updateSelected(selected);
        if (selected && !isEmpty()) {
            textField.requestFocus();
        }
    }

    @Override
    protected void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
            clearProperty();
        } else {
            initializeTextField();
            clearProperty();

            property = getExtractor().apply(getIndex());
            Bindings.bindBidirectional(textField.textProperty(), property, getConverter());

            setGraphic(textField);
            if (isSelected()) {
                textField.requestFocus();
            }
        }
    }

    private void clearProperty() {
        if (property != null) {
            Bindings.unbindBidirectional(textField.textProperty(), property);
            textField.setText(null);
            property = null;
        }
    }

    private void initializeTextField() {
        if (textField == null) {
            textField = new TextField();
            textField.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
                if (isFocused && !isSelected()) {
                    getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
                }
            });

            /*
             * TableView has key handlers that will select cells based on arrow keys being
             * pressed, scrolling to them if necessary. I find this mechanism looks cleaner
             * because, unlike TableView#scrollTo, it doesn't cause the cell to jump to the
             * top of the TableView.
             *
             * The way this works is by bypassing the TextField if, and only if, the event
             * is a KEY_PRESSED event and the pressed key is an arrow key. This lets the
             * event bubble up back to the TableView and let it do what it needs to. All
             * other key events are given to the TextField for normal processing.
             *
             * NOTE: The behavior being relied upon here is added by the default TableViewSkin
             *       and its corresponding TableViewBehavior. This may not work if a custom
             *       TableViewSkin skin is used.
             */
            EventDispatcher oldDispatcher = textField.getEventDispatcher();
            textField.setEventDispatcher((event, tail) -> {
                if (event.getEventType() == KeyEvent.KEY_PRESSED
                        && ((KeyEvent) event).getCode().isArrowKey()) {
                    return event;
                } else {
                    return oldDispatcher.dispatchEvent(event, tail);
                }
            });
        }
    }

}

注意事项

  1. 在使用 SelectionMode.MULTIPLE(并实际选择多个行/单元格)时,这两种方法都无法正常工作。
  2. TableView 上的 ObservableList 集不能定义 extractor。出于某种原因,这会导致表格在您输入TextField 时选择右下一个单元格。
  3. 仅使用 JavaFX 12 测试了这两种方法。

【讨论】:

  • 您确定可以禁用单元格选择吗?关闭后它不起作用。
  • 它适用于使用 JavaFX 12 的我。为了确保我什至明确地将 cellSelectionEnabled 设置为 false。请注意,我的示例不会将选择模型更新为与焦点模型同步,但它仍处于“行选择”模式。还有一些问题;例如,如果我在按箭头键之前输入了TextField,有时会得到IndexOutOfBoundsException——还不知道为什么。
  • 公平,focusedCell 最初为空(或未完全配置),这可能(或未尝试)解释 IOOB
  • @kleopatra 是的,这就是为什么在我的回答中我还必须添加这个:textField.focusedProperty().addListener((ObservableValue&lt;? extends Boolean&gt; o, Boolean oldValue, Boolean newValue) -&gt; { if (newValue) { getTableView().getFocusModel().focus(getTableRow().getIndex(), getTableColumn()); } }
  • hmm ...无法重现 IOOBE(在 fx11 中)-您能否添加一个示例说明它何时发生(最好的情况是我做错了什么,最坏的情况可能是是 fx12 中引入的一个错误;)此外,在 textField 中键入时的 UX 有点损坏:插入符号保持在 pos 0,因此预先挂起键入的字符并且左/右箭头键不会移动插入符号(没有奇怪,因为他们移动了focusedcell ;)
【解决方案2】:

感谢 Slaw。

首先启用单元格选择,table.getSelectionModel().setCellSelectionEnabled(true);

然后在 EditableTextCell.java 类中:

        this.focusedProperty().addListener((ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) ->
        {
            if (newValue)
            {
                textField.requestFocus();
            }

        });

        textField.focusedProperty().addListener((ObservableValue<? extends Boolean> o, Boolean oldValue, Boolean newValue) ->
        {
            if (newValue)
            {
                getTableView().getFocusModel().focus(getTableRow().getIndex(), getTableColumn());
            }
        }


        textField.setOnKeyPressed((KeyEvent ke) ->
        {
            switch (ke.getCode())
            {
                case DOWN:
                    getTableView().getFocusModel().focusBelowCell();
                    ke.consume();
                    break;
                case ENTER:
                    getTableView().getFocusModel().focusBelowCell();
                    ke.consume();
                    break;
                case UP:
                    getTableView().getFocusModel().focusAboveCell();
                    ke.consume();
                    break;
                case RIGHT:
                    getTableView().getFocusModel().focusRightCell();
                    ke.consume();
                    break;
                case LEFT:
                    getTableView().getFocusModel().focusLeftCell();
                    ke.consume();
                    break;
                default:
                    break;
            }
        });

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-20
    • 2015-10-31
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 1970-01-01
    • 2013-10-20
    • 2014-03-26
    相关资源
    最近更新 更多