JavaFX weird (Key)EventBehavior
所以我一直在用 javaFX 进行一些试验,我遇到了一些可能与
我将再次在这篇文章的底部发布一个工作示例,这样你就可以看到哪个单元格上到底发生了什么(包括调试!)。
我会尝试自己解释所有的行为,尽管这样更容易让你自己看到。基本上,使用
1:
如果您使用 contextMenu 添加新项目,则"escape"和"Enter"键(可能还有箭头键,尽管我现在不使用它们)的 keyEvents 在触发事件之前被消耗在单元格上(例如 textField 和单元格 KeyEvents!)虽然它正在触发父节点上的 keyEvent。 (在这种情况下为 AnchorPane)。
现在我知道了这些键被 contextMenu 默认行为捕获和使用的事实。虽然它不应该发生,因为在添加新项目后 contextMenu 已经隐藏。此外,textField 应该接收事件,尤其是当它集中时!
2:
当您使用 TableView 底部的按钮添加新项目时,会在 Parent 节点(AnchorPane)和 Cell 上触发 keyEvents。尽管 textField (即使聚焦时)根本没有收到任何 keyEvents。我无法解释为什么 TextField 即使在输入时也不会收到任何事件,所以我认为这肯定是一个错误?
3:
通过双击编辑单元格时,它会正确更新TableView的editingCellProperty(我检查了几次)。虽然通过 contextMenu 项目开始编辑(仅调用 startEdit() 用于测试目的)它不会正确更新编辑状态!有趣的是,它允许 keyEvents 像往常一样继续,不像情况 1
这是 Josh Bloch 的"继承打破封装"口头禅的典型代表。我的意思是,当您创建现有类的子类(在本例中为
我认为我不能解决每一个问题,但我可以在这里给出一些一般性的指导,并提供我认为可以实现您想要实现的目标的工作代码。
首先,细胞被重复使用。这是一件好事,因为它使表在有大量数据时执行得非常高效,但它使它变得复杂。基本思想是单元格基本上只为表格中的可见项创建。随着用户滚动,或随着表格内容的变化,不再需要的单元格被重新用于不同的可见项目。这大大节省了内存消耗和 CPU 时间(如果使用得当)。为了能够改进实现,JavaFX 团队故意不指定这是如何工作的,以及单元格可能被重用的方式和时间。因此,您必须小心假设单元格的项目或索引字段的连续性(反之,将哪个单元格分配给给定项目或索引),特别是如果您更改表格的结构。
你基本保证的是:
-
每当单元格被重新用于不同的项目时,都会在渲染单元格之前调用
updateItem() 方法。 -
每当单元格的索引发生变化时(可能是因为在列表中插入了一个项目,或者可能是因为单元格被重用,或两者兼而有之),在渲染单元格之前调用
updateIndex() 方法。
但是,请注意,如果两者都更改,则无法保证调用它们的顺序。因此,如果您的单元格渲染同时依赖于项目和索引(这里就是这种情况:您在 updateItem(...) 方法中检查项目和索引),您需要确保单元格在任一情况下更新这些属性的变化。实现这一点的最佳方法 (imo) 是创建一个私有方法来执行更新,并从 updateItem() 和 updateIndex() 委托给它。这样,当调用其中的第二个时,您的更新方法将以一致的状态调用。
如果您更改表格的结构,例如添加新行,则需要重新排列单元格,其中一些单元格可能会重复用于不同的项目(和索引)。但是,这种重新排列仅在表格布局时发生,默认情况下直到下一帧渲染才会发生。 (从性能的angular来看,这是有道理的:假设您在循环中对表格进行了 1000 次不同的更改;您不希望在每次更改时重新计算单元格,您只希望在下次将表格呈现为时重新计算它们屏幕。)这意味着,如果您向表中添加行,则不能依赖任何单元格的索引或项目是否正确。这就是为什么在添加新行后立即调用 table.edit(...) 是如此不可预测的原因。这里的技巧是在添加行后通过调用
请注意,当表格单元格获得焦点时按"Enter"将导致该单元格进入编辑模式。如果您使用键释放事件处理程序处理单元格中文本字段的提交,这些处理程序将以不可预知的方式进行交互。我认为这就是为什么您会看到奇怪的键处理效果(另请注意,文本字段会消耗它们在内部处理的键事件)。解决方法是在文本字段上使用 onAction 处理程序(无论如何,这可以说更具语义)。
不要让按钮静态(我不知道你为什么要这样做)。"静态"意味着按钮是整个类的属性,而不是该类的实例的属性。所以在这种情况下,所有单元格共享一个对单个按钮的引用。由于未指定单元重用机制,因此您不知道只有一个单元将按钮设置为其图形。这可能会导致灾难。例如,如果您将带有按钮的单元格滚动到视图之外,然后再返回到视图中,则无法保证当最后一个项目返回到视图中时,将使用相同的单元格来显示它。可能(我不知道实现方式)先前显示最后一个项目的单元格未被使用(可能是虚拟流容器的一部分,但被裁剪掉)并且没有更新。在这种情况下,该按钮将在场景图中出现两次,这将引发异常或导致不可预知的行为。基本上没有正当理由将场景图节点设为静态,这是一个特别糟糕的主意。
要编写这样的功能,您应该广泛阅读有关单元机制和
这是(我认为,我不确定我是否已经完全测试过)我认为您正在寻找的工作版本。我对结构做了一些细微的改动(不需要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | 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); } } |
我今天的重要学习项目(根据 James 的回答自由总结并略微扩展):
来强制稳定状态
下面是另一个可以玩的例子:
-
正如我的一条评论中已经提到的,它与 James 的不同之处在于,在监听器中开始编辑项目:可能并不总是最好的地方,具有单一位置的优势(至少就列表而言)涉及突变)用于布局调用。一个缺点是我们需要确定 viewSkin 的项目侦听器在我们之前被调用。为了保证这一点,只要皮肤发生变化,我们自己的监听器就会重新/注册。
-
作为重用练习,我扩展了 TextFieldTableCell 以额外处理按钮/菜单并根据行项目更新单元格的可编辑性。
-
表格外还有一些按钮可以试验:addAndEdit 和 scrollAndEdit。后者是为了证明"不稳定的细胞状态"可以通过不同于修改项目的路径来达到。
目前,我倾向于继承 TableView 并覆盖它的 edit(...) 以强制重新布局。类似于:
1 2 3 4 5 6 7 8 9 10 11 12 | public static class TTableView extends TableView { /** * Overridden to force a layout before calling super. */ @Override public void edit(int row, TableColumn<S, ?> column) { layout(); super.edit(row, column); } } |
做,减轻客户端代码的负担。不过,剩下的就是确保目标单元格滚动到可见区域。
例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 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","[email protected]"), new Person("Isabella","Johnson","[email protected]"), new Person("Ethan","Williams","[email protected]"), new Person("Emma","Jones","[email protected]"), new Person("Michael","Brown","[email protected]") , 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 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()); } |
更新
不应该太惊讶 - 半年前讨论了一个相关问题(并产生了错误报告)