如何将插入符号放置在用户在单元格中单击的位置

时间:2015-11-12 09:36:06

标签: java javafx position tableview caret

我希望我的可编辑TableView的行为与Excel工作表完全相同。这意味着双击任何单元格都应该将其置于编辑状态,并将插入符号放在用户在编辑时显示的文本字段中单击的位置(如果用户在文本结束后单击任意位置,则在文本末尾) )。

据我所知,奇怪命名的方法positionCaret(int)是用于将插入符号放在TextField中的方法。但是这个位置似乎是在字符中指定的,而不是在MouseEvent.getX()中可以获得的x位置值。

此外,只需尝试使用例如positionCaret(int)虚拟值“2”,无论如何都不太有效。有时插入符号位于第二个字符之后,有时它只是放在文本的开头。由于它似乎是完全随机的,我猜它是某种线程问题。

我在下面提供了一个MCVE。可编辑单元格的代码是TableView tutorial on docs.oracle的示例12-11的略微修改版本。

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;

public class TestEditableTable extends Application {
    public void start(Stage stage) {

        TableView<ObservableList<StringProperty>> table = new TableView<>();
        table.setEditable(true);
        table.getSelectionModel().setCellSelectionEnabled(true);

        // Makes sure that the cell isn't put into editing mode unless it is double clicked.
        table.addEventFilter(MouseEvent.MOUSE_PRESSED, new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent event) {

                // Only necessary in windows 7.
                if (event.isControlDown()) {
                    return;
                }

                if (table.getEditingCell() == null) {
                    table.getSelectionModel().clearSelection();
                }
            }
        });

        // Dummy columns
        ObservableList<String> columns = FXCollections.observableArrayList("Column1", "Column2", "Column3", "Column4",
                "Column5");

        // Dummy data
        ObservableList<StringProperty> row1 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
                new SimpleStringProperty("Cell2"), new SimpleStringProperty("0"), new SimpleStringProperty("Cell4"),
                new SimpleStringProperty("0"));
        ObservableList<StringProperty> row2 = FXCollections.observableArrayList(new SimpleStringProperty("Cell1"),
                new SimpleStringProperty("Cell2"), new SimpleStringProperty("1"), new SimpleStringProperty("Cell4"),
                new SimpleStringProperty("2"));
        ObservableList<ObservableList<StringProperty>> data = FXCollections.observableArrayList(row1, row2);

        for (int i = 0; i < columns.size(); i++) {
            final int j = i;
            TableColumn<ObservableList<StringProperty>, String> col = new TableColumn<>(columns.get(i));
            col.setCellValueFactory(param -> param.getValue().get(j));

            col.setCellFactory(e -> new EditingCell());

            table.getColumns().add(col);
        }

        table.setItems(data);

        Scene scene = new Scene(table);
        stage.setScene(scene);
        stage.show();
    }

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

    /**
     * An editable cell.
     */
    class EditingCell extends TableCell<ObservableList<StringProperty>, String> {

        private TextField textField;

        @Override
        public void startEdit() {
            if (!isEmpty()) {
                super.startEdit();
                createTextField();
                setText(null);
                setGraphic(textField);
            }
        }

        @Override
        public void cancelEdit() {
            super.cancelEdit();

            setText(getItem());
            setGraphic(null);
        }

        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);

            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                if (isEditing()) {
                    if (textField != null) {
                        textField.setText(item);
                    }
                    setText(null);
                    setGraphic(textField);
                } else {
                    setText(item);
                    setGraphic(null);
                }
            }
        }

        // Instantiates the text field.
        private void createTextField() {
            textField = new TextField(getItem());
            textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
            textField.setPadding(new Insets(0));

            // If the user defocus the textfield the edit should be commited. However, this only works properly if the focus is lost to another cell on the same row as far as I can tell.
            textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
                    if (!arg2) {
                        commitEdit(textField.getText());
                    }
                }
            });

            setOnMousePressed(e -> {
                // Gets the x coordinate of the cursor relative to the text field. Isn't used at the moment, but might prove useful.
                Double x = e.getX();

                Platform.runLater(() -> {
                    // The text field isn't focused unless we do this.
                    textField.requestFocus();

                    // Just testing positionCaret with some dummy value, doesn't seem to work properly.
                    textField.positionCaret(2);
                });
            });


        }
    }
}

1 个答案:

答案 0 :(得分:1)

也许考虑使用一个总是处于“编辑模式”的表格单元格实现:即它始终只显示一个文本字段,该文本字段双向绑定到相应的属性。这类似于标准CheckBoxTableCell的实现方式。与该单元格实现一样,单元格永远不会进入和退出“编辑模式”,因此永远不会调用startEditcommitEditcancelEdit方法,CellEditEvent s永远不会在列上触发,但显示的数据会不断与模型中的属性同步。

以下是一个示例:

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class AlwaysEditingTable extends Application {

    private final static int NUM_COLUMNS = 10 ;

    @Override
    public void start(Stage primaryStage) {
        TableView<List<String>> table = new TableView<>();
        FilteredList<List<String>> filteredList = populateTableData(table);

        for (int colIndex = 0; colIndex < NUM_COLUMNS; colIndex++) {
            final int c = colIndex ;
            table.getColumns().add(column("Column "+(colIndex+1), 
                    (List<String> row) -> new ReadOnlyStringWrapper(row.get(c)), 
                    (List<String> row, String newText) -> row.set(c, newText)));
        }



        Button debugButton = new Button("Print content");
        debugButton.setOnAction(e -> 
            table.getItems().stream().map(row -> String.join("\t", row)).forEach(System.out::println)
        );

        TextField filter = new TextField();
        filter.textProperty().addListener((obs, oldValue, newValue) -> 
            filteredList.setPredicate(row -> row.stream().filter(data -> data.startsWith(newValue)).findAny().isPresent())
        );
        filter.setPromptText("Enter filter");

        BorderPane root = new BorderPane(table, filter, null, debugButton, null);
        BorderPane.setMargin(debugButton, new Insets(10));
        BorderPane.setAlignment(debugButton, Pos.CENTER);

        primaryStage.setScene(new Scene(root, 880, 600));
        primaryStage.show();
    }

    private <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property, BiConsumer<S,String> updater) {
        TableColumn<S,T> column = new TableColumn<>(title);
        column.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        column.setCellFactory(c -> new AlwaysEditingCell<S,T>(updater));
        return column ;
    }

    private FilteredList<List<String>> populateTableData(TableView<List<String>> table) {

        ObservableList<List<String>> data = FXCollections.observableArrayList() ;
        for (int i = 1 ; i <= 30 ; i++) {
            List<String> row = new ArrayList<>();
            for (int colIndex = 0; colIndex < NUM_COLUMNS ; colIndex++) {
                row.add("Data ["+(colIndex+1)+", "+i+"]");
            }
            data.add(row);
        }

        FilteredList<List<String>> filteredList = new FilteredList<>(data);
        table.setItems(filteredList);
        return filteredList ;
    }



    public static class AlwaysEditingCell<S,T> extends TableCell<S,T> {

        private final TextField textField ;

        /**
         * 
         * @param updater A function that acts on the current row value and the 
         * text in the text field to update the row value according to the new 
         * text. This function is invoked whenever the text in the text field changes.
         */
        public AlwaysEditingCell(BiConsumer<S,String> updater) {

            textField = new TextField();

            this.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
                if (isNowEmpty) {
                    setGraphic(null);
                } else {
                    setGraphic(textField);
                }
            });

            textField.textProperty().addListener((obs, oldText, newText) -> 
                updater.accept(getTableView().getItems().get(getIndex()), newText));
        }

        @Override
        public void updateItem(T item, boolean empty) {
            if (empty) {
                setGraphic(null);
            } else {
                String value = item.toString() ;
                if (! value.equals(textField.getText())) {
                    textField.setText(value);
                }
                setGraphic(textField);
            }
        }

    }

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