如何防止 TextFieldTableCell 在按下 ENTER 键时进入编辑模式?

How do you prevent a TextFieldTableCell from going into Edit mode when pressing the ENTER key?

默认情况下,在 TextFieldTableCell 上按 ENTER 会将单元格置于编辑模式并在单元格中绘制文本字段。我该如何防止这种情况?如果按下数字键,我只希望单元格进入编辑模式。如果按下 ENTER,那么它应该只是 select 下面的单元格。

我尝试过的:

tableView.setOnKeyReleased(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        TablePosition tp;
        if(event.getCode().isDigitKey()) {
            lastKey = event.getText();
            tp = tableView.getFocusModel().getFocusedCell();
            tableView.edit(tp.getRow() , tp.getTableColumn());
        }
        if(tableView.getEditingCell() == null && event.getCode() == KeyCode.ENTER) {
            event.consume();
            tableView.getSelectionModel().selectBelowCell();
        }
    }
});

单元格仍进入编辑模式。

也许您可以使用 Event Filter 来防止在按下回车键时进入编辑模式(以及 select 下面的单元格):

tableView.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
    if(keyEvent.getCode() == KeyCode.ENTER) {
        tableView.getSelectionModel().selectBelowCell();
        keyEvent.consume();
    }
});

按下数字键开始编辑模式:

tableView.setOnKeyPressed(event -> {
    TablePosition<Person, ?> pos = tableView.getFocusModel().getFocusedCell() ;
    if (pos != null && event.getCode().isDigitKey()) {
        tableView.edit(pos.getRow(), pos.getTableColumn());
    }
});

编辑:包含示例

package org.example;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

import java.util.stream.IntStream;

public class TableViewTestApp extends Application {

    @Override
    public void start(Stage stage) {
        stage.setScene(new Scene(createTableView()));
        stage.show();
    }

    private TableView<Item> createTableView() {
        TableView<Item> tableView = new TableView<>();
        tableView.setEditable(true);
        tableView.getSelectionModel().setCellSelectionEnabled(true);

        TableColumn<Item, String> nameCol = new TableColumn<>("name");
        tableView.getColumns().add(nameCol);

        nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

        nameCol.setCellFactory(c -> new CustomTableCell());

        // Enter edit mode on digit pressed only
        tableView.setOnKeyPressed(event -> {
            TablePosition<Item, ?> pos = tableView.getFocusModel().getFocusedCell();
            if (pos != null && event.getCode().isDigitKey()) {
                tableView.edit(pos.getRow(), pos.getTableColumn());
            }
        });

        tableView.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
            if (keyEvent.getCode() == KeyCode.ENTER) {
                tableView.getSelectionModel().selectBelowCell(); //***When last cell of column move to the first cell of next column maybe
                keyEvent.consume();
            }
        });

        IntStream.range(1, 5).forEach(i -> tableView.getItems().add(new Item("Item " + i)));

        return tableView;
    }

    private class CustomTableCell extends TableCell<Item, String> {

        private TextField textField;

        private void createTextField() {
            textField = new TextField(getItem());

            // Commit value on focus lost:
            textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
                if (wasFocused) {
                    commitEdit(textField.getText());
                }
            });

            // This is needed to commit last cell of the column on enter released:
            // (***Alternative: Move to first cell of next column)
            textField.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit(getItem());
                    keyEvent.consume();
                }
            });
        }

        @Override
        public void startEdit() {
            super.startEdit();
            setText(null);
            createTextField();
            setGraphic(textField);
            textField.requestFocus();
            textField.selectAll();
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(getItem());
            setGraphic(null);
        }

        // See: https://gist.github.com/james-d/be5bbd6255a4640a5357
        @Override
        public void commitEdit(String item) {
            // This block is necessary to support commit on losing focus, because the baked-in mechanism
            // sets our editing state to false before we can intercept the loss of focus.
            // The default commitEdit(...) method simply bails if we are not editing...
            if (!isEditing() && !item.equals(getItem())) {
                TableView<Item> table = getTableView();
                if (table != null) {
                    TableColumn<Item, String> column = getTableColumn();
                    TableColumn.CellEditEvent<Item, String> event = new TableColumn.CellEditEvent<>(table,
                            new TablePosition<>(table, getIndex(), column),
                            TableColumn.editCommitEvent(), item);
                    Event.fireEvent(column, event);
                }
            }
            super.commitEdit(item);
            setText(item);
            setGraphic(null);
            textField = null;
        }

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (item == null || empty) {
                setText(null);
            } else {
                setText(item);
            }
        }
    }

    private class Item {

        private final StringProperty name;

        public Item(String name) {
            this.name = new SimpleStringProperty(name);
        }

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(String name) {
            this.name.set(name);
        }
    }

    public static void main(String[] args) {
        launch();
    }
}

编辑 2:为整数添加了自定义 table 单元格(带微调器)

package org.example;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.WritableValue;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;

import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;

public class TableViewTestApp extends Application {

    @Override
    public void start(Stage stage) {
        TableView<Item> tableView = createTableView();
        Button btn = new Button("print items to console");
        btn.setOnAction(actionEvent -> tableView.getItems().forEach(System.out::println));
        stage.setScene(new Scene(new VBox(tableView, btn)));
        stage.show();
    }

    private TableView<Item> createTableView() {
        TableView<Item> tableView = new TableView<>();
        tableView.setEditable(true);
        tableView.getSelectionModel().setCellSelectionEnabled(true);

        TableColumn<Item, String> nameCol = new TableColumn<>("name");
        tableView.getColumns().add(nameCol);

        nameCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

        nameCol.setCellFactory(c -> new StringTextFieldTableCell());

        // Enter edit mode on digit pressed only
        tableView.setOnKeyPressed(event -> {
            TablePosition<Item, ?> pos = tableView.getFocusModel().getFocusedCell();
            if (pos != null && event.getCode().isDigitKey()) {
                tableView.edit(pos.getRow(), pos.getTableColumn());
            }
        });

        tableView.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
            if (keyEvent.getCode() == KeyCode.ENTER) {
                tableView.getSelectionModel().selectBelowCell(); //***When last cell of column move to the first cell of next column maybe
                keyEvent.consume();
            }
        });

        IntStream.range(1, 5).forEach(i -> tableView.getItems().add(new Item("Item " + i)));

        TableColumn<Item, Integer> quantityCol = new TableColumn<>("quantity");
        quantityCol.setCellValueFactory(cellData -> cellData.getValue().quantityProperty().asObject());
        quantityCol.setCellFactory(c -> new IntegerSpinnerTableCell<>());
        quantityCol.setMinWidth(150);
        tableView.getColumns().add(quantityCol);

        return tableView;
    }

    private class StringTextFieldTableCell extends TableCell<Item, String> {

        private TextField textField;

        private void createTextField() {
            textField = new TextField(getItem());

            // Commit value on focus lost:
            textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
                if (wasFocused) {
                    commitEdit(textField.getText());
                }
            });

            // This is needed to commit last cell of the column on enter released:
            // (***Alternative: Move to first cell of next column)
            textField.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit(getItem());
                    keyEvent.consume();
                }
            });
        }

        // set the text of the text field and display the graphic
        @Override
        public void startEdit() {
            super.startEdit();
            setText(null);
            createTextField();
            setGraphic(textField);
            textField.requestFocus();
            textField.selectAll();
        }

        // revert to text display
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(getItem());
            setGraphic(null);
        }

        // See: https://gist.github.com/james-d/be5bbd6255a4640a5357
        @Override
        public void commitEdit(String item) {
            // This block is necessary to support commit on losing focus, because the baked-in mechanism
            // sets our editing state to false before we can intercept the loss of focus.
            // The default commitEdit(...) method simply bails if we are not editing...
            if (!isEditing() && !item.equals(getItem())) {
                TableView<Item> table = getTableView();
                if (table != null) {
                    TableColumn<Item, String> column = getTableColumn();
                    TableColumn.CellEditEvent<Item, String> event = new TableColumn.CellEditEvent<>(table,
                            new TablePosition<>(table, getIndex(), column),
                            TableColumn.editCommitEvent(), item);
                    Event.fireEvent(column, event);
                }
            }
            super.commitEdit(item);
            setText(item);
            setGraphic(null);
            textField = null;
        }

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (item == null || empty)
                setText(null);
            else
                setText(item);
        }
    }

    public class IntegerSpinnerTableCell<T> extends TableCell<T, Integer> {

        private final Spinner<Integer> spinner;
        private final SpinnerValueFactory.IntegerSpinnerValueFactory valueFactory;
        private boolean ignoreUpdate;

        public IntegerSpinnerTableCell() {
            spinner = new Spinner<>();
            valueFactory = new SpinnerValueFactory.IntegerSpinnerValueFactory(1, Integer.MAX_VALUE, 1, 1);
            spinner.setValueFactory(valueFactory);
            spinner.setEditable(true);
            NumberFormat format = NumberFormat.getIntegerInstance();
            UnaryOperator<TextFormatter.Change> filter = c -> {
                if (c.isContentChange()) {
                    ParsePosition parsePosition = new ParsePosition(0);
                    format.parse(c.getControlNewText(), parsePosition);
                    if (parsePosition.getIndex() == 0 || parsePosition.getIndex() < c.getControlNewText().length()) {
                        return null;
                    }
                }
                return c;
            };

            TextFormatter<Integer> intFormatter = new TextFormatter<>(new IntegerStringConverter(), 1, filter);
            spinner.getEditor().setTextFormatter(intFormatter);

            spinner.valueProperty().addListener((o, oldValue, newValue) -> {
                if (!ignoreUpdate) {
                    ignoreUpdate = true;
                    WritableValue<Number> property = (WritableValue<Number>) getTableColumn()
                            .getCellObservableValue(getTableRow().getItem());
                    property.setValue(newValue);
                    ignoreUpdate = false;
                }
            });

            spinner.getEditor().addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit(getItem());
                    keyEvent.consume();
                }
            });
        }

        @Override
        protected void updateItem(Integer item, boolean empty) {
            super.updateItem(item, empty);
            if (item == null || empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    ignoreUpdate = true;
                    setText(null);
                    valueFactory.setValue(item);
                    setGraphic(spinner);
                    ignoreUpdate = false;
                } else {
                    ignoreUpdate = true;
                    setText(String.valueOf(item));
                    setGraphic(null);
                    ignoreUpdate = false;
                }
            }
        }

        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                ignoreUpdate = true;
                setText(null);
                setGraphic(spinner);
                valueFactory.setValue(getItem());
                requestEditorAndSelectText(Integer.MAX_VALUE);
                ignoreUpdate = false;
            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(String.valueOf(getItem()));
            setGraphic(null);
        }

        @Override
        public void commitEdit(Integer newValue) {
            if (!isEditing()) {
                TableView<T> table = getTableView();
                if (table != null) {
                    TableColumn<T, Integer> column = getTableColumn();
                    TableColumn.CellEditEvent<T, Integer> event = new TableColumn.CellEditEvent<>(table,
                            new TablePosition<>(table, getIndex(), column),
                            TableColumn.editCommitEvent(), spinner.getValue());
                    Event.fireEvent(column, event);
                }
            }
            super.commitEdit(newValue);
            setText(String.valueOf(newValue.intValue()));
            setGraphic(null);
        }

        private void requestEditorAndSelectText(int maxTries) {
            if (maxTries > 0) {
                Platform.runLater(() -> {
                            if (!spinner.getEditor().isFocused()) {
                                spinner.getEditor().requestFocus();
                                spinner.getEditor().selectAll();
                                requestEditorAndSelectText(maxTries - 1);
                            }
                        }
                );
            }
        }
    }

    private class Item {

        private final StringProperty name;
        private final IntegerProperty quantity;

        public Item(String name) {
            this.name = new SimpleStringProperty(name);
            this.quantity = new SimpleIntegerProperty(1);
        }

        @Override
        public String toString() {
            return getName() + " | " + getQuantity();
        }

        public String getName() {
            return name.get();
        }

        public StringProperty nameProperty() {
            return name;
        }

        public void setName(String name) {
            this.name.set(name);
        }

        public int getQuantity() {
            return quantity.get();
        }

        public IntegerProperty quantityProperty() {
            return quantity;
        }

        public void setQuantity(int quantity) {
            this.quantity.set(quantity);
        }
    }


    public static void main(String[] args) {
        launch();
    }
}

编辑 3:为 IntegerTextFieldTableCell 添加示例(没有 Spinner,仅 TextField):

package org.example;

import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;

import java.text.NumberFormat;
import java.text.ParsePosition;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;

public class TableWithPlainTextFieldTableCellForIntTestApp extends Application {

    @Override
    public void start(Stage stage) {

        TableView<Item> tableView = new TableView<>();
        tableView.setEditable(true);
        tableView.getSelectionModel().setCellSelectionEnabled(true);

        TableColumn<Item, Integer> quantityCol = new TableColumn<>("quantity");
        tableView.getColumns().add(quantityCol);

        quantityCol.setCellValueFactory(cellData -> cellData.getValue().quantityProperty().asObject());
        quantityCol.setCellFactory(c -> new IntegerTextFieldTableCell());
        // Enter edit mode on digit pressed only

        tableView.setOnKeyPressed(event -> {
            TablePosition<Item, ?> pos = tableView.getFocusModel().getFocusedCell();
            if (pos != null && event.getCode().isDigitKey()) {
                tableView.edit(pos.getRow(), pos.getTableColumn());
            }
        });

        tableView.addEventFilter(KeyEvent.KEY_PRESSED, keyEvent -> {
            if (keyEvent.getCode() == KeyCode.ENTER) {
                tableView.getSelectionModel().selectBelowCell();
                keyEvent.consume();
            }
        });

        IntStream.range(1, 5).forEach(i -> tableView.getItems().add(new Item()));

        quantityCol.setMinWidth(150);

        Button printBtn = new Button("print items");
        printBtn.setOnAction(actionEvent -> tableView.getItems().forEach(System.out::println));

        stage.setScene(new Scene(new VBox(tableView, printBtn)));
        stage.show();
    }

    class IntegerTextFieldTableCell extends TableCell<Item, Integer> {

        private TextField textField;

        private void createTextField() {
            textField = new TextField(getItem().toString());

            // Commit value on focus lost:
            textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
                if (wasFocused) {
                    try {
                        commitEdit(Integer.parseInt(textField.getText()));
                    } catch (NumberFormatException ignored) {
                        commitEdit(1);
                    }
                }
            });

            // This is needed to commit last cell of the column on enter released:
            // (***Alternative: Move to first cell of next column)
            textField.addEventFilter(KeyEvent.KEY_RELEASED, keyEvent -> {
                if (keyEvent.getCode() == KeyCode.ENTER) {
                    commitEdit(getItem());
                    keyEvent.consume();
                }
            });

            NumberFormat format = NumberFormat.getIntegerInstance();
            UnaryOperator<TextFormatter.Change> filter = c -> {
                if (c.isContentChange()) {
                    ParsePosition parsePosition = new ParsePosition(0);
                    format.parse(c.getControlNewText(), parsePosition);
                    if (parsePosition.getIndex() == 0 || parsePosition.getIndex() < c.getControlNewText().length()) {
                        return null;
                    }
                }
                return c;
            };
            TextFormatter<Integer> intFormatter = new TextFormatter<>(new IntegerStringConverter(), 1, filter);
            textField.setTextFormatter(intFormatter);
        }

        // set the text of the text field and display the graphic
        @Override
        public void startEdit() {
            super.startEdit();
            setText(null);
            createTextField();
            setGraphic(textField);
            textField.requestFocus();
            textField.selectAll();
        }

        // revert to text display
        @Override
        public void cancelEdit() {
            super.cancelEdit();
            setText(getItem().toString());
            setGraphic(null);
        }

        // See: https://gist.github.com/james-d/be5bbd6255a4640a5357
        @Override
        public void commitEdit(Integer item) {
            // This block is necessary to support commit on losing focus, because the baked-in mechanism
            // sets our editing state to false before we can intercept the loss of focus.
            // The default commitEdit(...) method simply bails if we are not editing...
            if (!isEditing() && !item.equals(getItem())) {
                TableView<Item> table = getTableView();
                if (table != null) {
                    TableColumn<Item, Integer> column = getTableColumn();
                    TableColumn.CellEditEvent<Item, Integer> event = new TableColumn.CellEditEvent<>(table,
                            new TablePosition<>(table, getIndex(), column),
                            TableColumn.editCommitEvent(), item);
                    Event.fireEvent(column, event);
                }
            }
            super.commitEdit(item);
            setText(item.toString());
            setGraphic(null);
            textField = null;
        }

        @Override
        protected void updateItem(Integer item, boolean empty) {
            super.updateItem(item, empty);
            if (item == null || empty)
                setText(null);
            else
                setText(item.toString());
        }
    }

    class Item {

        private final IntegerProperty quantity = new SimpleIntegerProperty(1);

        @Override
        public String toString() {
            return quantity.get() + "";
        }

        public int getQuantity() {
            return quantity.get();
        }

        public IntegerProperty quantityProperty() {
            return quantity;
        }

        public void setQuantity(int quantity) {
            this.quantity.set(quantity);
        }
    }

    public static void main(String[] args) {
        launch();
    }
}

在 table 上手动安装 filters/handlers 的替代方法是调整 table 的 InputMap。当心:那是私有的 api,需要访问内部包(输入映射和行为)和从皮肤反射抓取行为。希望这些软件包很快就会进入 public 范围。

基本方法是

  • 从皮肤中获取行为及其 inputMap
  • 删除要替换的键映射
  • 根据需要添加新的键映射

在代码中:

TableViewSkin<?> skin = (TableViewSkin<?>) table.getSkin();
// use your favorite utility method to reflectively access the private field
TableViewBehavior<?> listBehavior = (TableViewBehavior<?>) FXUtils.invokeGetFieldValue(
        TableViewSkin.class, skin, "behavior");
InputMap<?> map = listBehavior.getInputMap();
// remove old mapping for enter
Optional<Mapping<?>> mapping = map.lookupMapping(new KeyBinding(ENTER));
map.getMappings().remove(mapping.get());
// add new mapping for enter
map.getMappings().add(new KeyMapping(KeyCode.ENTER, e -> table.getSelectionModel().selectBelowCell()));
// add mappings for digits
EventHandler<KeyEvent> handler = e -> {
    TablePosition<T, ?> pos = table.getFocusModel().getFocusedCell() ;
    if (pos != null ) {
        table.edit(pos.getRow(), pos.getTableColumn());
    }
};
List<KeyMapping> digitMappings = Arrays.stream(KeyCode.values())
        .filter(c -> c.isDigitKey())
        .map(c -> new KeyMapping(c, handler))
        .collect(Collectors.toList());
                
map.getMappings().addAll(digitMappings);

显然,这需要 table 的皮肤可用并且它是 TableViewSkin 类型。要使用,可以在 table 的皮肤 属性:

的侦听器中调用它
table.skinProperty().addListener(c -> {
    tweakInputMap(table);
});

应该在 fx 版本 >= fx9 中工作(我的上下文是 fx17/18 dev)并且需要在编译时添加导出 inputmap/behavior 包并在运行时添加打开这些包。