JavaFX很奇怪(Key)EventBehavior

时间:2016-02-08 21:09:34

标签: events javafx tableview javafx-8 keyboard-events

所以我一直在尝试使用javaFX,但我遇到了一些可能与TableView#edit()方法相关的奇怪行为。

我会再次在这篇文章的底部发布一个工作示例,这样你就可以看到究竟在哪个单元格上发生了什么(包括debuging!)。

我会尝试自己解释所有的行为,虽然它更容易为自己看到它。基本上,使用TableView#edit()方法时,事件会搞乱。

1

如果你使用contextMenu添加一个新项目,那么键'escape'和'Enter'的键事件(以及可能的箭头键,虽然我现在不使用它们)在它们触发事件之前被消耗掉在单元格上(例如textField和cell KeyEvents!)虽然它在父节点上触发了keyEvent。 (在这种情况下是AnchorPane)。

现在我知道这些键是由contextMenu默认行为捕获和使用的。虽然不应该发生这种情况,因为在添加新项目之后contextMenu已经被隐藏了。 textField应该接收更多事件,特别是当它集中时!

2:

当您使用TableView底部的按钮添加新项时,将在Parent节点(AnchorPane)和Cell上触发keyEvents。虽然textField(即使在聚焦时)也没有收到任何keyEvents。我无法解释为什么TextField即使在输入时也不会收到任何事件,所以我认为这绝对是一个bug?

3:

通过双击编辑单元格时,它会正确更新TableView的editingCellProperty(我检查了几次)。虽然在开始编辑contextMenu项目时(它只为testpurpose调用startEdit())它没有正确更新编辑状态!有趣的是它允许keyEvents像往常一样继续,不像情况1& 2。

4:

当你编辑一个项目,然后添加一个项目(任何一种方式都会导致这个问题)它会将editingCellProperty更新为当前单元格,但是当停止编辑时,它会以某种方式恢复到最后一个单元格?!?那是有趣的事情发生的部分,我真的无法解释。

注意startEdit()& cancelEdit()方法在奇怪的时刻被调用,并在错误的单元格上调用!

现在我不明白这个逻辑。如果这是预期的行为,将非常感谢对它的一些解释!

这是一个例子:

package testpacket;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;



public class EditStateTest extends Application
{
    private static ObservableList<SimpleStringProperty> exampleList = FXCollections.observableArrayList();
    //Placeholder for the button
    private static SimpleStringProperty PlaceHolder = new SimpleStringProperty();

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

    @Override
    public void start(Stage primaryStage) throws Exception
    {
        // basic ui setup
        AnchorPane parent = new AnchorPane();
        Scene scene = new Scene(parent);
        primaryStage.setScene(scene);

        //fill backinglist with data
        for(int i = 0 ; i < 20; i++)
            exampleList.add(new SimpleStringProperty("Hello Test"));
        exampleList.add(PlaceHolder);

        //create a basic tableView
        TableView<SimpleStringProperty> listView = new TableView<SimpleStringProperty>();
        listView.setEditable(true);

        TableColumn<SimpleStringProperty, String> column = new TableColumn<SimpleStringProperty, String>();
        column.setCellFactory(E -> new TableCellTest<SimpleStringProperty, String>());
        column.setCellValueFactory(E -> E.getValue());
        column.setEditable(true);

        // set listViews' backing list
        listView.setItems(exampleList);


        listView.getColumns().clear();
        listView.getColumns().add(column);
        parent.getChildren().add(listView);

        parent.setOnKeyReleased(E -> System.out.println("Parent - KeyEvent"));


        primaryStage.show();
    }

    // basic editable cell example
    public static class TableCellTest<S, T> extends TableCell<S, T>
    {
        // The editing textField.
        protected static Button addButton = new Button("Add");
        protected TextField textField = new TextField();;
        protected ContextMenu menu;


        public TableCellTest()
        {
            this.setOnContextMenuRequested(E -> {
                if(this.getTableView().editingCellProperty().get() == null)
                    this.menu.show(this, E.getScreenX(), E.getScreenY());
            });
            this.menu = new ContextMenu();

            MenuItem createNew = new MenuItem("create New");
            createNew.setOnAction(E -> {
                System.out.println("Cell ContextMenu " + this.getIndex() + " - createNew: onAction");
                this.onNewItem(this.getIndex() + 1);
            });
            MenuItem edit = new MenuItem("edit");
            edit.setOnAction(E -> {
                System.out.println("Cell ContextMenu " + this.getIndex() + " - edit: onAction");
                this.startEdit();
            });

            this.menu.getItems().setAll(createNew, edit);

            addButton.addEventHandler(ActionEvent.ACTION, E -> {
                if(this.getIndex() == EditStateTest.exampleList.size() - 1)
                {
                    System.out.println("Cell " + this.getIndex() + " - Button: onAction");
                    this.onNewItem(this.getIndex());
                }
            });
            addButton.prefWidthProperty().bind(this.widthProperty());

            this.setOnKeyReleased(E -> System.out.println("Cell " + this.getIndex() + " - KeyEvent"));
        }

        public void onNewItem(int index)
        {
            EditStateTest.exampleList.add(index, new SimpleStringProperty("New Item"));
            this.getTableView().edit(index, this.getTableColumn());
            textField.requestFocus();
        }

        @Override
        public void startEdit()
        {
            if (!isEditable()
                    || (this.getTableView() != null && !this.getTableView().isEditable())
                    || (this.getTableColumn() != null && !this.getTableColumn().isEditable()))
                return;

            System.out.println("Cell " + this.getIndex() + " - StartEdit");
            super.startEdit();

            this.createTextField();

            textField.setText((String)this.getItem());
            this.setGraphic(textField);
            textField.selectAll();
            this.setText(null);
        }

        @Override
        public void cancelEdit()
        {
            if (!this.isEditing())
                return;

            System.out.println("Cell " + this.getIndex() + " - CancelEdit");
            super.cancelEdit();

            this.setText((String)this.getItem());
            this.setGraphic(null);
        }

        @Override
        protected void updateItem(T item, boolean empty)
        {
            System.out.println("Cell " + this.getIndex() + " - UpdateItem");
            super.updateItem(item, empty);

            if(empty || item == null)
            {
                if(this.getIndex() == EditStateTest.exampleList.size() - 1)
                {
                    this.setText("");
                    this.setGraphic(addButton);
                }
                else
                {
                    this.setText(null);
                    this.setGraphic(null);
                }
            }
            else
            {
                // These checks are needed to make sure this cell is the specific cell that is in editing mode.
                // Technically this#isEditing() can be left out, as it is not accurate enough at this point.
                if(this.getTableView().getEditingCell() != null 
                        && this.getTableView().getEditingCell().getRow() == this.getIndex())
                {
                    //change to TextField
                    this.setText(null);
                    this.setGraphic(textField);
                }
                else
                {
                    //change to actual value
                    this.setText((String)this.getItem());
                    this.setGraphic(null);
                }
            }
        }

        @SuppressWarnings("unchecked")
        public void createTextField()
        {
            textField.setOnKeyReleased(E -> {
                System.out.println("TextField " + this.getIndex() + " - KeyEvent");
                System.out.println(this.getTableView().getEditingCell());
//              if(this.getTableView().getEditingCell().getRow() == this.getIndex())
                    if(E.getCode() == KeyCode.ENTER)
                    {
                        this.setItem((T) textField.getText());
                        this.commitEdit(this.getItem());
                    }
                    else if(E.getCode() == KeyCode.ESCAPE)
                        this.cancelEdit();
            });
        }
    }
}

我希望有人可以帮我进一步解决这个问题。如果您对此有任何建议/解决方案或解决方法,请告诉我们! 谢谢你的时间!

2 个答案:

答案 0 :(得分:3)

这是Josh Bloch&#34;继承打破封装的典型代表&#34;口头禅。我的意思是,当你创建一个现有类的子类(在这种情况下是TableCell)时,你需要了解很多关于该类的实现以便制作子类与超类很好地配合。你在代码中做了很多假设,关于TableView和它的单元格之间的交互是不正确的,并且(以及一些控件中的一些错误和一般奇怪的事件处理实现)是你的代码是断。

我不认为我可以解决每一个问题,但我可以在这里给出一些一般性的指示,并提供我认为可以实现你想要实现的目标的工作代码。

首先,重复使用细胞。这是一件好事,因为当数据量很大时,它会使表格非常有效,但这会让它变得复杂。基本思想是单元格基本上只为表格中的可见项目创建。当用户滚动时,或者当表格内容发生变化时,不再需要的单元格将被重用于可见的不同项目。这大大节省了内存消耗和CPU时间(如果使用得当)。为了能够改进实现,JavaFX团队故意不指定其工作原理,以及如何以及何时可以重用单元。因此,您必须小心对单元格的项目或索引字段的连续性做出假设(相反,将哪个单元格分配给给定的项目或索引),尤其是在更改表格的结构时。

基本保证的是:

  • 每次将单元格重用于其他项目时,都会在呈现单元格之前调用updateItem()方法。
  • 任何时候单元格的索引发生更改(可能是因为项目已插入列表中,或者可能是因为单元格被重用,或两者都有),在单元格之前调用updateIndex()方法呈现。

但是,请注意,在两者都发生变化的情况下,不能保证调用它们的顺序。因此,如果您的单元格渲染依赖于项目和索引(这里是这种情况:您在updateItem(...)方法中同时检查项目和索引),则需要确保单元格更新时这些属性的变化。实现此目的的最佳方式(imo)是创建一个私有方法来执行更新,并从updateItem()和updateIndex()委托它。这样,当调用其中的第二个时,将使用一致状态调用update方法。

如果更改表格的结构,例如添加新行,则需要重新排列单元格,其中一些可能会重复用于不同的项目(和索引)。但是,这种重新排列仅在表格布局时发生,默认情况下,直到下一帧渲染才会发生。 (从性能角度来看,这是有道理的:假设您在循环中对表进行了1000次不同的更改;您不希望在每次更改时重新计算单元格,您只需要在下次表格重新计算时重新计算它们渲染到屏幕。)这意味着,如果向表中添加行,则不能依赖任何单元格的索引或项目是否正确。这就是为什么在添加新行后立即调用table.edit(...)是如此不可预测。这里的技巧是在添加行之后通过调用TableView.layout()来强制执行表的布局。

请注意,按&#34;输入&#34;当表格单元格被聚焦时,将导致该单元格进入编辑模式。如果使用密钥释放事件处理程序处理单元格中文本字段的提交,则这些处理程序将以不可预测的方式进行交互。我认为这就是为什么你会看到你看到的奇怪的键处理效果(同时注意文本字段消耗他们内部处理的关键事件)。解决方法是在文本字段上使用onAction处理程序(无论如何,这可能更具语义性)。

不要让按钮静止(我不知道你为什么要这样做)。 &#34;静态&#34;表示按钮是整个类的属性,而不是该类的实例。因此,在这种情况下,所有单元格共享对单个按钮的引用。由于未指定单元格重用机制,因此您不知道只有一个单元格将按钮设置为其图形。这可能会导致灾难。例如,如果您使用按钮滚动单元格,然后返回视图,则无法保证在返回视图时将使用相同的单元格显示最后一个项目。有可能(我不知道实现)先前显示最后一项的单元格是未使用的(可能是虚拟流容器的一部分,但是被截去视图外)并且未更新。在这种情况下,按钮将在场景图中出现两次,这将导致异常或导致不可预测的行为。基本上没有正当理由让场景图节点静止,这是一个特别糟糕的主意。

要编写此类功能,您应该广泛阅读cell mechanismTableViewTableColumnTableCell的文档。在某些时候,您可能会发现需要深入了解source code以了解所提供的单元格实现是如何工作的。

这里(我认为,我不确定我已经完全测试过)我认为你正在寻找的工作版本。我对结构做了一些细微的修改(不需要StringProperty作为数据类型,String只要没有相同的重复项就可以正常工作),添加onEditCommit处理程序等等。

import javafx.application.Application;
import javafx.beans.value.ObservableValueBase;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class TableViewWithAddAtEnd extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<String> table = new TableView<>();
        table.setEditable(true);

        TableColumn<String, String> column = new TableColumn<>("Data");
        column.setPrefWidth(150);
        table.getColumns().add(column);

        // use trivial wrapper for string data:
        column.setCellValueFactory(cellData -> new ObservableValueBase<String>() {
            @Override
            public String getValue() {
                return cellData.getValue();
            }
        });

        column.setCellFactory(col -> new EditingCellWithMenuEtc());

        column.setOnEditCommit(e -> 
            table.getItems().set(e.getTablePosition().getRow(), e.getNewValue()));

        for (int i = 1 ; i <= 20; i++) {
            table.getItems().add("Item "+i);
        }
        // blank for "add" button:
        table.getItems().add("");

        BorderPane root = new BorderPane(table);
        primaryStage.setScene(new Scene(root, 600, 600));
        primaryStage.show();

    }

    public static class EditingCellWithMenuEtc extends TableCell<String, String> {
        private TextField textField ;
        private Button button ;
        private ContextMenu contextMenu ;

        // The update relies on knowing both the item and the index
        // Since we don't know (or at least shouldn't rely on) the order
        // in which the item and index are updated, we just delegate
        // implementations of both updateItem and updateIndex to a general
        // method. This way doUpdate() is always called last with consistent
        // state, so we are guaranteed to be in a consistent state when the
        // cell is rendered, even if we are temporarily in an inconsistent 
        // state between the calls to updateItem and updateIndex.

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            doUpdate(item, getIndex(), empty);
        }

        @Override
        public void updateIndex(int index) {
            super.updateIndex(index);
            doUpdate(getItem(), index, isEmpty());
        }

        // update the cell. This updates the text, graphic, context menu
        // (empty cells and the special button cell don't have context menus)
        // and editable state (empty cells and the special button cell can't
        // be edited)
        private void doUpdate(String item, int index, boolean empty) {
            if (empty) {
                setText(null);
                setGraphic(null);
                setContextMenu(null);
                setEditable(false);
            } else {
                if (index == getTableView().getItems().size() - 1) {
                    setText(null);
                    setGraphic(getButton());
                    setContextMenu(null);
                    setEditable(false);
                } else if (isEditing()) {
                    setText(null);
                    getTextField().setText(item);
                    setGraphic(getTextField());
                    getTextField().requestFocus();
                    setContextMenu(null);
                    setEditable(true);
                } else {
                    setText(item);
                    setGraphic(null);
                    setContextMenu(getMenu());
                    setEditable(true);
                }
            }
        }

        @Override
        public void startEdit() {
            if (! isEditable() 
                    || ! getTableColumn().isEditable()
                    || ! getTableView().isEditable()) {
                return ;
            }
            super.startEdit();
            getTextField().setText(getItem());
            setText(null);
            setGraphic(getTextField());
            setContextMenu(null);
            textField.selectAll();
            textField.requestFocus();
        }

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

        @Override
        public void commitEdit(String newValue) {
            // note this fires onEditCommit handler on column:
            super.commitEdit(newValue);
            setText(getItem());
            setGraphic(null);
            setContextMenu(getMenu());
        }

        private void addNewItem(int index) {
            getTableView().getItems().add(index, "New Item");
            // force recomputation of cells:
            getTableView().layout();
            // start edit:
            getTableView().edit(index, getTableColumn());
        }

        private ContextMenu getMenu() {
            if (contextMenu == null) {
                createContextMenu();
            }
            return contextMenu ;
        }

        private void createContextMenu() {
            MenuItem addNew = new MenuItem("Add new");
            addNew.setOnAction(e -> addNewItem(getIndex() + 1));
            MenuItem edit = new MenuItem("Edit");
            // note we call TableView.edit(), not this.startEdit() to ensure 
            // table's editing state is kept consistent:
            edit.setOnAction(e -> getTableView().edit(getIndex(), getTableColumn()));
            contextMenu = new ContextMenu(addNew, edit);
        }

        private Button getButton() {
            if (button == null) {
                createButton();
            }
            return button ;
        }

        private void createButton() {
            button = new Button("Add");
            button.prefWidthProperty().bind(widthProperty());
            button.setOnAction(e -> addNewItem(getTableView().getItems().size() - 1));
        }

        private TextField getTextField() {
            if (textField == null) {
                createTextField();
            }
            return textField ;
        }

        private void createTextField() {
            textField = new TextField();
            // use setOnAction for enter, to avoid conflict with enter on cell:
            textField.setOnAction(e -> commitEdit(textField.getText()));
            // use key released for escape: note text fields do note consume
            // key releases they don't handle:
            textField.setOnKeyReleased(e -> {
                if (e.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                }
            });
        }
    }

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

答案 1 :(得分:1)

我当天的重要学习项目(自由汇总并稍微扩展自James' answer):

只有当所有单元格都处于稳定状态并且目标单元格可见时,

view.edit(...)才能安全地调用。大多数情况下,我们可以通过调用view.layout()

来强制稳定状态

下面是另一个使用的例子:

  • 正如我在其中一篇评论中所提到的,它与James'在听众中开始编辑项目不同:可能并不总是最好的地方,具有单一位置的优势(至少如布局调用涉及列表突变)。缺点是我们需要确定viewSkin对项目的监听器是在我们之前调用的。为了保证这一点,我们自己的监听器会在皮肤发生变化时重新/注册。

  • 作为重复使用的练习,我将TextFieldTableCell扩展为另外处理按钮/菜单,并根据行项更新单元格的可编辑性。

  • 表格外还有一些按钮可以试验:addAndEdit和scrollAndEdit。后者是为了证明与修改项目不同的路径可以达到“不稳定的细胞状态”。

目前,我倾向于继承TableView并覆盖其编辑(...)以强制重新布局。类似的东西:

public static class TTableView<S> extends TableView<S> {

    /**
     * Overridden to force a layout before calling super.
     */
    @Override
    public void edit(int row, TableColumn<S, ?> column) {
        layout();
        super.edit(row, column);
    }

}

这样做可以减轻客户端代码的负担。剩下的就是确保目标单元格滚动到可见区域。

示例:

public class TablePersonAddRowAndEdit extends Application {

    private PersonStandIn standIn = new PersonStandIn();
    private final ObservableList<Person> data =
            // Person from Tutorial - with Properties exposed!
            FXCollections.observableArrayList(
                    new Person("Jacob", "Smith", "jacob.smith@example.com"),
                    new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
                    new Person("Ethan", "Williams", "ethan.williams@example.com"),
                    new Person("Emma", "Jones", "emma.jones@example.com"),
                    new Person("Michael", "Brown", "michael.brown@example.com")
                    , standIn
                    );


    private Parent getContent() {

        TableView<Person> table = new TableView<>();
        table.setItems(data);
        table.setEditable(true);

        TableColumn<Person, String> firstName = new TableColumn<>("First Name");
        firstName.setCellValueFactory(new PropertyValueFactory<>("firstName"));

        firstName.setCellFactory(v -> new MyTextFieldCell<>());
        ListChangeListener l = c -> {
            while (c.next()) {
                // true added only
                if (c.wasAdded() && ! c.wasRemoved()) {
                    // force the re-layout before starting the edit
                    table.layout();
                    table.edit(c.getFrom(), firstName);
                    return;
                }
            };
        };
        // install the listener to the items after the skin has registered
        // its own
        ChangeListener skinListener = (src, ov, nv) -> {
            table.getItems().removeListener(l);
            table.getItems().addListener(l);
        };
        table.skinProperty().addListener(skinListener);

        table.getColumns().addAll(firstName);

        Button add = new Button("AddAndEdit");
        add.setOnAction(e -> {
            int standInIndex = table.getItems().indexOf(standIn);
            int index = standInIndex < 0 ? table.getItems().size() : standInIndex;
            index =1;
            Person person = createNewItem("edit", index);
            table.getItems().add(index, person);

        });
        Button edit = new Button("Edit");
        edit.setOnAction(e -> {
            int index = 1;//table.getItems().size() -2;
            table.scrollTo(index);
            table.requestFocus();
            table.edit(index, firstName);
        });
        HBox buttons = new HBox(10, add, edit);
        BorderPane content = new BorderPane(table);
        content.setBottom(buttons);
        return content;
    }

    /**
     * A cell that can handle not-editable items. Has to update its
     * editability based on the rowItem. Must be done in updateItem
     * (tried a listener to the tableRow's item, wasn't good enough - doesn't
     * get notified reliably)
     * 
     */
    public static class MyTextFieldCell<S> extends TextFieldTableCell<S, String> {

        private Button button;

        public MyTextFieldCell() {
            super(new DefaultStringConverter());
            ContextMenu menu = new ContextMenu();
            menu.getItems().add(createMenuItem());
            setContextMenu(menu);
        }

        private boolean isStandIn() {
            return getTableRow() != null && getTableRow().getItem() instanceof StandIn;
        }

        /**
         * Update cell's editable based on the rowItem.
         */
        private void doUpdateEditable() {
            if (isEmpty() || isStandIn()) {
                setEditable(false);
            } else {
                setEditable(true);
            }
        }

        @Override
        public void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            doUpdateEditable();
            if (isStandIn()) {
                if (isEditing()) {
                    LOG.info("shouldn't be editing - has StandIn");
                }
                if (button == null) {
                    button = createButton();
                }
                setText(null);
                setGraphic(button);
            } 
        }

        private Button createButton() {
            Button b = new Button("Add");
            b.setOnAction(e -> {
                int index = getTableView().getItems().size() -1;
                getTableView().getItems().add(index, createNewItem("button", index));
            });
            return b;
        }

        private MenuItem createMenuItem() {
            MenuItem item = new MenuItem("Add");
            item.setOnAction(e -> {
                if (isStandIn()) return;
                int index = getIndex();
                getTableView().getItems().add(index, createNewItem("menu", index));
            });
            return item;
        }


        private S createNewItem(String text, int index) {
            return (S) new Person(text + index, text + index, text);
        }

    }

    private Person createNewItem(String text, int index) {
        return new Person(text + index, text + index, text);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        primaryStage.setScene(new Scene(getContent()));
        primaryStage.setTitle(FXUtils.version());
        primaryStage.show();
    }

    /**
     * Marker-Interface to denote a class as not mutable.
     */
    public static interface StandIn {
    }

    public static class PersonStandIn extends Person implements StandIn{

         public PersonStandIn() {
            super("standIn", "", "");
        }

    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(TablePersonAddRowAndEdit.class.getName());
}

更新

不应该太惊讶 - 半年前曾讨论related problem(并制作了bug report