关于事件:JavaFX 怪异(Key)EventBehavior
JavaFX weird (Key)EventBehavior
所以我一直在用 javaFX 进行一些试验,我遇到了一些可能与 TableView#edit() 方法有关的相当奇怪的行为。
我将再次在这篇文章的底部发布一个工作示例,这样你就可以看到哪个单元格上到底发生了什么(包括调试!)。
我会尝试自己解释所有的行为,尽管这样更容易让你自己看到。基本上,使用 TableView#edit() 方法时事件会搞砸。
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
相关讨论
- 浏览源代码(相当广泛),我认为是表格视图本身消耗了输入和转义键。
- @James_D 是的,我也发现了(还能是什么?)。虽然它很奇怪,因为你会期望它从下到上调用,而不是反之亦然。而且它并不总是从焦点节点开始,这对我来说也很有趣。我假设它会调用焦点节点,并继续切换到"最后调用节点"的父节点,并在该节点中引发特定事件,直到它到达顶部。
这是 Josh Bloch 的"继承打破封装"口头禅的典型代表。我的意思是,当您创建现有类的子类(在本例中为 TableCell)时,您需要了解很多关于该类的实现,以使子类与超类很好地配合。您在代码中对 TableView 与其单元格之间的交互做出了很多假设,这些假设是不正确的,并且(以及一些错误和某些控件中事件处理的一般奇怪实现)是您的代码破坏的原因。
我认为我不能解决每一个问题,但我可以在这里给出一些一般性的指导,并提供我认为可以实现您想要实现的目标的工作代码。
首先,细胞被重复使用。这是一件好事,因为它使表在有大量数据时执行得非常高效,但它使它变得复杂。基本思想是单元格基本上只为表格中的可见项创建。随着用户滚动,或随着表格内容的变化,不再需要的单元格被重新用于不同的可见项目。这大大节省了内存消耗和 CPU 时间(如果使用得当)。为了能够改进实现,JavaFX 团队故意不指定这是如何工作的,以及单元格可能被重用的方式和时间。因此,您必须小心假设单元格的项目或索引字段的连续性(反之,将哪个单元格分配给给定项目或索引),特别是如果您更改表格的结构。
你基本保证的是:
- 每当单元格被重新用于不同的项目时,都会在渲染单元格之前调用 updateItem() 方法。
- 每当单元格的索引发生变化时(可能是因为在列表中插入了一个项目,或者可能是因为单元格被重用,或两者兼而有之),在渲染单元格之前调用 updateIndex() 方法。
但是,请注意,如果两者都更改,则无法保证调用它们的顺序。因此,如果您的单元格渲染同时依赖于项目和索引(这里就是这种情况:您在 updateItem(...) 方法中检查项目和索引),您需要确保单元格在任一情况下更新这些属性的变化。实现这一点的最佳方法 (imo) 是创建一个私有方法来执行更新,并从 updateItem() 和 updateIndex() 委托给它。这样,当调用其中的第二个时,您的更新方法将以一致的状态调用。
如果您更改表格的结构,例如添加新行,则需要重新排列单元格,其中一些单元格可能会重复用于不同的项目(和索引)。但是,这种重新排列仅在表格布局时发生,默认情况下直到下一帧渲染才会发生。 (从性能的angular来看,这是有道理的:假设您在循环中对表格进行了 1000 次不同的更改;您不希望在每次更改时重新计算单元格,您只希望在下次将表格呈现为时重新计算它们屏幕。)这意味着,如果您向表中添加行,则不能依赖任何单元格的索引或项目是否正确。这就是为什么在添加新行后立即调用 table.edit(...) 是如此不可预测的原因。这里的技巧是在添加行后通过调用 TableView.layout() 来强制表格的布局。
请注意,当表格单元格获得焦点时按"Enter"将导致该单元格进入编辑模式。如果您使用键释放事件处理程序处理单元格中文本字段的提交,这些处理程序将以不可预知的方式进行交互。我认为这就是为什么您会看到奇怪的键处理效果(另请注意,文本字段会消耗它们在内部处理的键事件)。解决方法是在文本字段上使用 onAction 处理程序(无论如何,这可以说更具语义)。
不要让按钮静态(我不知道你为什么要这样做)。"静态"意味着按钮是整个类的属性,而不是该类的实例的属性。所以在这种情况下,所有单元格共享一个对单个按钮的引用。由于未指定单元重用机制,因此您不知道只有一个单元将按钮设置为其图形。这可能会导致灾难。例如,如果您将带有按钮的单元格滚动到视图之外,然后再返回到视图中,则无法保证当最后一个项目返回到视图中时,将使用相同的单元格来显示它。可能(我不知道实现方式)先前显示最后一个项目的单元格未被使用(可能是虚拟流容器的一部分,但被裁剪掉)并且没有更新。在这种情况下,该按钮将在场景图中出现两次,这将引发异常或导致不可预知的行为。基本上没有正当理由将场景图节点设为静态,这是一个特别糟糕的主意。
要编写这样的功能,您应该广泛阅读有关单元机制和 TableView、TableColumn 和 TableCell 的文档。在某些时候,您可能会发现您需要深入研究源代码以了解所提供的单元实现是如何工作的。
这是(我认为,我不确定我是否已经完全测试过)我认为您正在寻找的工作版本。我对结构做了一些细微的改动(不需要 StringPropertys 作为数据类型,只要你没有相同的重复项,String 就可以正常工作),添加了一个 onEditCommit 处理程序等。
|
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 TableColumn<String, String> column = new TableColumn<>("Data"); // use trivial wrapper for string data: column.setCellFactory(col -> new EditingCellWithMenuEtc()); column.setOnEditCommit(e -> for (int i = 1 ; i <= 20; i++) { BorderPane root = new BorderPane(table); } public static class EditingCellWithMenuEtc extends TableCell<String, String> { // The update relies on knowing both the item and the index @Override @Override // update the cell. This updates the text, graphic, context menu @Override @Override @Override private void addNewItem(int index) { private ContextMenu getMenu() { private void createContextMenu() { private Button getButton() { private void createButton() { private TextField getTextField() { private void createTextField() { public static void main(String[] args) { |
相关讨论
- 有趣的是布局确实有效,但在我的实验中没有(过去几天有很多!-并再次告诉我完整的编辑子系统有多么古怪:-(我将 table.edit 放入侦听器中项目 - 我认为是自然位置 - 它没有帮助,因为尚未安装皮肤。所以要么必须强制安装皮肤,要么延迟注册直到安装皮肤。
- 唔。这是一个有趣的灰色地带。如果您只是通过 UI 更新项目列表,那么通过项目列表上的侦听器启动编辑是有意义的。但是,如果您是在外部更新它(例如从服务),那么它真的不会。在这种情况下,可能应该以某种方式存在其他模型层(视图模型)。我只能通过(黑客)将 table.layout(); table.edit(...); package在 runLater() 中来使其与项目列表上的侦听器一起工作。但是在这种情况下肯定安装了皮肤吗?那么这真的是问题还是"正在修改的列表"的事情?
我今天的重要学习项目(根据 James 的回答自由总结并略微扩展):
view.edit(...) 只有在所有单元格都处于稳定状态并且目标单元格可见时才能安全调用。大多数时候我们可以通过调用 view.layout()
来强制稳定状态
下面是另一个可以玩的例子:
-
正如我的一条评论中已经提到的,它与 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 {
/** } |
做,减轻客户端代码的负担。不过,剩下的就是确保目标单元格滚动到可见区域。
例子:
|
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 Parent getContent() { TableView<Person> table = new TableView<>(); TableColumn<Person, String> firstName = new TableColumn<>("First Name"); firstName.setCellFactory(v -> new MyTextFieldCell<>()); table.getColumns().addAll(firstName); Button add = new Button("AddAndEdit"); }); /** private Button button; public MyTextFieldCell() { private boolean isStandIn() { /** @Override private Button createButton() { private MenuItem createMenuItem() {
private S createNewItem(String text, int index) { } private Person createNewItem(String text, int index) { @Override /** public static class PersonStandIn extends Person implements StandIn{ public PersonStandIn() { } public static void main(String[] args) { @SuppressWarnings("unused") |
更新
不应该太惊讶 - 半年前讨论了一个相关问题(并产生了错误报告)