事件-JavaFX怪异(Key)EventBehavior

因此,我一直在用javaFX对其进行试验,我遇到了一些可能与TableView#edit()方法有关的怪异行为.

我将在该文章的底部再次发布一个工作示例,以便您可以看到在哪个单元上到底发生了什么(包括调试!).

我将尝试自己解释所有行为,尽管这种方式更容易自己观察.基本上,使用TableView#edit()方法时会弄乱事件.

1:

如果您正在使用contextMenu添加新项目,则在它们触发单元格上的事件之前,将消耗键“转义”和“回车”(可能是箭头键,尽管我现在不使用它们)的keyEvents (例如,textField和单元格KeyEvents!)尽管它正在父节点上触发keyEvent. (在这种情况下为AnchorPane).

现在,我知道一个事实,这些密钥是由contextMenu的默认行为捕获和使用的.尽管应该不会发生这种情况,因为添加新项之后contextMenu已经被隐藏了.此外,textField应该接收事件,尤其是在重点关注时!

2:

当您使用TableView底部的按钮添加新的Item时,将在父节点(AnchorPane)和单元格上触发keyEvent.尽管textField(即使处于焦点状态)也完全不接收keyEvent.我无法解释为什么即使输入时TextField也不接收任何事件,所以我认为那肯定是一个错误?

3:

通过双击编辑单元格时,它会正确更新TableView的editingCellProperty(我检查了几次).尽管通过contextMenu项(仅出于测试目的仅调用startEdit())开始编辑时,它无法正确更新编辑状态!有趣的是,与情况1和情况不同,它允许keyEvents照常继续进行. 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();
            });
        }
    }
}

我希望有人可以进一步帮助我.如果您对此有建议/解决方案或解决方法,请告诉我!
谢谢你的时间!

最佳答案
这是乔什·布洛赫(Josh Bloch)的“继承打破封装”口号的典型代表.我的意思是,当您创建现有类的子类(在本例中为TableCell)时,您需要了解有关该类的实现的很多知识,才能使该子类与超类很好地协作.您在代码中对TableView及其单元之间的交互进行了很多不正确的假设,这就是代码破裂的原因(以及一些错误以及某些控件中事件处理的一般怪异实现).

我认为我无法解决每个问题,但是我可以在此处提供一些一般性的提示,并提供可以实现您要实现的目标的有效代码.

首先,单元被重用.这是一件好事,因为它使表在有大量数据时非常有效地执行,但是却使其变得复杂.基本思想是,基本上只为表格中的可见项创建单元格.随着用户的滚动或表内容的更改,不再需要的单元格将用于可见的不同项目.如果使用正确,则可以大大节省内存消耗和CPU时间.为了能够改善实现,JavaFX团队故意不指定其工作方式以及可能以及何时重用单元的方式.因此,必须对单元格的项目或索引字段的连续性(以及相反,将哪个单元格分配给给定的项目或索引)进行假设,尤其是在更改表的结构时.

您基本上可以保证的是:

>每当单元格被用于其他项目时,在呈现单元格之前都会调用updateItem()方法.
>每当单元格的索引发生更改时(这可能是因为在列表中插入了一项,或者可能是因为单元格已被重用,或者是因为这两项都被重用),所以在呈现单元格之前将调用updateIndex()方法.

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

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

请注意,将焦点放在表格单元格上时按“ Enter”将使该单元格进入编辑模式.如果使用键释放事件处理程序处理单元格中文本字段上的提交,则这些处理程序将以不可预测的方式进行交互.我认为这就是为什么您会看到奇怪的键处理效果的原因(还请注意,文本字段消耗了它们在内部处理的键事件).解决该问题的方法是在文本字段上使用onAction处理程序(无论如何,它可能更具有语义).

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

要编写这样的功能,应广泛阅读cell mechanism以及TableViewTableColumnTableCell的文档.有时,您可能会发现需要深入研究source code才能了解提供的单元实现的工作方式.

这是(我认为我不确定我是否已完全测试过)我认为您正在寻找的工作版本.我对结构进行了一些细微的更改(数据类型不需要StringPropertys,只要没有相同的重复项,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);
    }
}

转载注明原文:事件-JavaFX怪异(Key)EventBehavior - 代码日志