所以我一直在尝试使用javaFX,但我遇到了一些可能与TableView#edit()
方法相关的奇怪行为。
我会再次在这篇文章的底部发布一个工作示例,这样你就可以看到究竟在哪个单元格上发生了什么(包括debuging!)。
我会尝试自己解释所有的行为,虽然它更容易为自己看到它。基本上,使用TableView#edit()
方法时,事件会搞乱。
如果你使用contextMenu添加一个新项目,那么键'escape'和'Enter'的键事件(以及可能的箭头键,虽然我现在不使用它们)在它们触发事件之前被消耗掉在单元格上(例如textField和cell KeyEvents!)虽然它在父节点上触发了keyEvent。 (在这种情况下是AnchorPane)。
现在我知道这些键是由contextMenu默认行为捕获和使用的。虽然不应该发生这种情况,因为在添加新项目之后contextMenu已经被隐藏了。 textField应该接收更多事件,特别是当它集中时!
当您使用TableView底部的按钮添加新项时,将在Parent节点(AnchorPane)和Cell上触发keyEvents。虽然textField(即使在聚焦时)也没有收到任何keyEvents。我无法解释为什么TextField即使在输入时也不会收到任何事件,所以我认为这绝对是一个bug?
通过双击编辑单元格时,它会正确更新TableView的editingCellProperty(我检查了几次)。虽然在开始编辑contextMenu项目时(它只为testpurpose调用startEdit())它没有正确更新编辑状态!有趣的是它允许keyEvents像往常一样继续,不像情况1& 2。
当你编辑一个项目,然后添加一个项目(任何一种方式都会导致这个问题)它会将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();
});
}
}
}
我希望有人可以帮我进一步解决这个问题。如果您对此有任何建议/解决方案或解决方法,请告诉我们! 谢谢你的时间!
答案 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 mechanism和TableView
,TableColumn
和TableCell
的文档。在某些时候,您可能会发现需要深入了解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)