TableView 不会在焦点丢失事件上提交值
Posted
技术标签:
【中文标题】TableView 不会在焦点丢失事件上提交值【英文标题】:TableView doesn't commit values on focus lost event 【发布时间】:2015-06-17 01:35:51 【问题描述】:我想创建一个具有以下功能的表格:
按键编辑 输入键 = 下一行 Tab 键 = 下一列 退出键 = 取消编辑以下是实现这些功能的代码。价值观应该致力于失去焦点。问题:他们没有承诺。触发焦点更改事件,根据控制台输出值将是正确的,但最终表格单元格中的值是旧值。
有谁知道如何防止这种情况以及如何获取当前的 EditingCell 对象以便我可以手动调用提交?毕竟应该调用某种验证器,如果值不正确,它会阻止更改焦点。
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TableViewInlineEditDemo extends Application
private final TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
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"));
public static void main(String[] args)
launch(args);
@Override
public void start(Stage stage)
Scene scene = new Scene(new Group());
stage.setWidth(450);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
Callback<TableColumn<Person, String>, TableCell<Person, String>> cellFactory = (TableColumn<Person, String> p) -> new EditingCell();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
firstNameCol.setCellFactory(cellFactory);
firstNameCol.setOnEditCommit((CellEditEvent<Person, String> t) ->
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
);
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
lastNameCol.setCellFactory(cellFactory);
lastNameCol.setOnEditCommit((CellEditEvent<Person, String> t) ->
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setLastName(t.getNewValue());
);
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));
emailCol.setCellFactory(cellFactory);
emailCol.setOnEditCommit((CellEditEvent<Person, String> t) ->
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setEmail(t.getNewValue());
);
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
// edit mode on keypress
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>()
@Override
public void handle(KeyEvent e)
if( e.getCode() == KeyCode.TAB) // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectNext();
e.consume();
return;
else if( e.getCode() == KeyCode.ENTER) // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectBelowCell();
e.consume();
return;
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null)
if( e.getCode().isLetterKey() || e.getCode().isDigitKey())
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
);
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
final VBox vbox = new VBox();
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
class EditingCell extends TableCell<Person, String>
private TextField textField;
public EditingCell()
@Override
public void startEdit()
if (!isEmpty())
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus(); // must be before selectAll() or the caret would be in wrong position
textField.selectAll();
@Override
public void cancelEdit()
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
@Override
public void updateItem(String item, boolean empty)
super.updateItem(item, empty);
if (empty)
setText(null);
setGraphic(null);
else
if (isEditing())
if (textField != null)
textField.setText(getString());
setText(null);
setGraphic(textField);
else
setText(getString());
setGraphic(null);
private void createTextField()
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
// commit on focus lost
textField.focusedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) ->
if( oldValue = true && newValue == false)
System.out.println( "Focus lost, current value: " + textField.getText());
commitEdit();
);
// cancel edit on ESC
textField.addEventFilter(KeyEvent.KEY_RELEASED, e ->
if( e.getCode() == KeyCode.ESCAPE)
cancelEdit();
);
private String getString()
return getItem() == null ? "" : getItem().toString();
private boolean commitEdit()
super.commitEdit(textField.getText());
return true; // TODO: add verifier and check if commit was possible
public static class Person
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email)
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
public String getFirstName()
return firstName.get();
public void setFirstName(String fName)
firstName.set(fName);
public String getLastName()
return lastName.get();
public void setLastName(String fName)
lastName.set(fName);
public String getEmail()
return email.get();
public void setEmail(String fName)
email.set(fName);
非常感谢!
编辑:我已经缩小了范围。似乎问题在于JavaFX代码在焦点更改时取消了编辑模式。这很糟糕。
public Cell()
setText(null); // default to null text, to match the null item
// focusTraversable is styleable through css. Calling setFocusTraversable
// makes it look to css like the user set the value and css will not
// override. Initializing focusTraversable by calling set on the
// CssMetaData ensures that css will be able to override the value.
((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
getStyleClass().addAll(DEFAULT_STYLE_CLASS);
/**
* Indicates whether or not this cell has focus. For example, a
* ListView defines zero or one cell as being the "focused" cell. This cell
* would have focused set to true.
*/
super.focusedProperty().addListener(new InvalidationListener()
@Override public void invalidated(Observable property)
pseudoClassStateChanged(PSEUDO_CLASS_FOCUSED, isFocused()); // TODO is this necessary??
// The user has shifted focus, so we should cancel the editing on this cell
if (!isFocused() && isEditing())
cancelEdit();
);
// initialize default pseudo-class state
pseudoClassStateChanged(PSEUDO_CLASS_EMPTY, true);
【问题讨论】:
【参考方案1】:我找到了一个简单的解决方案,适用于我的 TableCells。这个想法是在焦点丢失时忘记 commitEdit。让 javafx 完成它的工作,然后只更新之前编辑的单元格的值。
abstract class EditingTextCell<T, V> extends TableCell<T, V>
protected TextField textField;
private T editedItem;
@Override
public void startEdit()
...
textField.focusedProperty().addListener((t, oldval, newval) ->
if (!newval)
setItemValue(editedItem, textField.getText());
);
editedItem = (T) getTableRow().getItem();
public abstract void setItemValue(T item, String text);
...
所以,唯一的技巧是实现 setItemValue() 以更新项目的正确部分。
【讨论】:
hmm .. 你不一定会得到 textField 的 focusLost - 这就是为什么这个问题很难解决的部分原因 ;) @kleopatra 我在编辑时单击不同的表格单元格时得到它。目标是在这种情况下保存编辑。【参考方案2】:我遇到了同样的问题,我通过结合这两个代码 sn-ps 解决了它:
https://gist.github.com/james-d/be5bbd6255a4640a5357 https://gist.github.com/abhinayagarwal/9383881自定义 TableCell 实现
public class EditCell<S, T> extends TableCell<S, T>
private final TextField textField = new TextField();
// Converter for converting the text in the text field to the user type, and vice-versa:
private final StringConverter<T> converter;
/**
* Creates and initializes an edit cell object.
*
* @param converter
* the converter to convert from and to strings
*/
public EditCell(StringConverter<T> converter)
this.converter = converter;
itemProperty().addListener((obx, oldItem, newItem) ->
setText(newItem != null ? this.converter.toString(newItem) : null);
);
setGraphic(this.textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
this.textField.setOnAction(evt ->
commitEdit(this.converter.fromString(this.textField.getText()));
);
this.textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) ->
if (!isNowFocused)
commitEdit(this.converter.fromString(this.textField.getText()));
);
this.textField.addEventFilter(KeyEvent.KEY_PRESSED, event ->
if (event.getCode() == KeyCode.ESCAPE)
this.textField.setText(this.converter.toString(getItem()));
cancelEdit();
event.consume();
else if (event.getCode() == KeyCode.TAB)
commitEdit(this.converter.fromString(this.textField.getText()));
TableColumn<S, ?> nextColumn = getNextColumn(!event.isShiftDown());
if (nextColumn != null)
getTableView().getSelectionModel().clearAndSelect(getTableRow().getIndex(), nextColumn);
getTableView().edit(getTableRow().getIndex(), nextColumn);
);
/**
* Convenience converter that does nothing (converts Strings to themselves and vice-versa...).
*/
public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>()
@Override
public String toString(String object)
return object;
@Override
public String fromString(String string)
return string;
;
/**
* Convenience method for creating an EditCell for a String value.
*
* @return the edit cell
*/
public static <S> EditCell<S, String> createStringEditCell()
return new EditCell<S, String>(IDENTITY_CONVERTER);
// set the text of the text field and display the graphic
@Override
public void startEdit()
super.startEdit();
this.textField.setText(this.converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
this.textField.requestFocus();
// revert to text display
@Override
public void cancelEdit()
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
// commits the edit. Update property if possible and revert to text display
@Override
public void commitEdit(T item)
// This block is necessary to support commit on losing focus, because the baked-in mechanism
// sets our editing state to false before we can intercept the loss of focus.
// The default commitEdit(...) method simply bails if we are not editing...
if (!isEditing() && !item.equals(getItem()))
TableView<S> table = getTableView();
if (table != null)
TableColumn<S, T> column = getTableColumn();
CellEditEvent<S, T> event = new CellEditEvent<>(table,
new TablePosition<S, T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
/**
* Finds and returns the next editable column.
*
* @param forward
* indicates whether to search forward or backward from the current column
* @return the next editable column or @code null if there is no next column available
*/
private TableColumn<S, ?> getNextColumn(boolean forward)
List<TableColumn<S, ?>> columns = new ArrayList<>();
for (TableColumn<S, ?> column : getTableView().getColumns())
columns.addAll(getEditableColumns(column));
// There is no other column that supports editing.
if (columns.size() < 2) return null;
int currentIndex = columns.indexOf(getTableColumn());
int nextIndex = currentIndex;
if (forward)
nextIndex++;
if (nextIndex > columns.size() - 1)
nextIndex = 0;
else
nextIndex--;
if (nextIndex < 0)
nextIndex = columns.size() - 1;
return columns.get(nextIndex);
/**
* Returns all editable columns of a table column (supports nested columns).
*
* @param root
* the table column to check for editable columns
* @return a list of table columns which are editable
*/
private List<TableColumn<S, ?>> getEditableColumns(TableColumn<S, ?> root)
List<TableColumn<S, ?>> columns = new ArrayList<>();
if (root.getColumns().isEmpty())
// We only want the leaves that are editable.
if (root.isEditable())
columns.add(root);
return columns;
else
for (TableColumn<S, ?> column : root.getColumns())
columns.addAll(getEditableColumns(column));
return columns;
控制器
@FXML
private void initialize()
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
table.getItems().addAll(
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")
);
table.setOnKeyPressed(event ->
TablePosition<Person, ?> pos = table.getFocusModel().getFocusedCell() ;
if (pos != null && event.getCode().isLetterKey())
table.edit(pos.getRow(), pos.getTableColumn());
);
private <T> TableColumn<T, String> createColumn(String title, Function<T, StringProperty> property)
TableColumn<T, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> EditCell.createStringEditCell());
return col;
【讨论】:
【参考方案3】:我解决这个暴行的建议如下(抱歉错过了 JavaDoc)。
这是一个取消提交重定向解决方案。我在 LINUX 下使用 Java 1.8.0-121 对其进行了测试。在这里,放弃单元格编辑器的唯一方法是按 ESCAPE。
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public abstract class AutoCommitTableCell<S,T> extends TableCell<S,T>
private Node field;
private boolean startEditing;
private T defaultValue;
/** @return a newly created input field. */
protected abstract Node newInputField();
/** @return the current value of the input field. */
protected abstract T getInputValue();
/** Sets given value to the input field. */
protected abstract void setInputValue(T value);
/** @return the default in case item is null, must be never null, else cell will not be editable. */
protected abstract T getDefaultValue();
/** @return converts the given value to a string, being the cell-renderer representation. */
protected abstract String inputValueToText(T value);
@Override
public void startEdit()
try
startEditing = true;
super.startEdit(); // updateItem() will be called
setInputValue(getItem());
finally
startEditing = false;
/** Redirects to commitEdit(). Leaving the cell should commit, just ESCAPE should cancel. */
@Override
public void cancelEdit()
// avoid JavaFX NullPointerException when calling commitEdit()
getTableView().edit(getIndex(), getTableColumn());
commitEdit(getInputValue());
private void cancelOnEscape()
if (defaultValue != null) // canceling default means writing null
setItem(defaultValue = null);
setText(null);
setInputValue(null);
super.cancelEdit();
@Override
protected void updateItem(T newValue, boolean empty)
if (startEditing && newValue == null)
newValue = (defaultValue = getDefaultValue());
super.updateItem(newValue, empty);
if (empty || newValue == null)
setText(null);
setGraphic(null);
else
setText(inputValueToText(newValue));
setGraphic(startEditing || isEditing() ? getInputField() : null);
protected final Node getInputField()
if (field == null)
field = newInputField();
// a cell-editor won't be committed or canceled automatically by JFX
field.addEventFilter(KeyEvent.KEY_PRESSED, event ->
if (event.getCode() == KeyCode.ENTER || event.getCode() == KeyCode.TAB)
commitEdit(getInputValue());
else if (event.getCode() == KeyCode.ESCAPE)
cancelOnEscape();
);
contentDisplayProperty().bind(
Bindings.when(editingProperty())
.then(ContentDisplay.GRAPHIC_ONLY)
.otherwise(ContentDisplay.TEXT_ONLY)
);
return field;
您可以扩展此类以支持任何数据类型。
字符串字段的示例是(Person 是一个示例 bean):
import javafx.scene.Node;
import javafx.scene.control.TextField;
import jfx.examples.tablebinding.PersonsModel.Person;
public class StringTableCell extends AutoCommitTableCell<Person,String>
@Override
protected String getInputValue()
return ((TextField) getInputField()).getText();
@Override
protected void setInputValue(String value)
((TextField) getInputField()).setText(value);
@Override
protected String getDefaultValue()
return "";
@Override
protected Node newInputField()
return new TextField();
@Override
protected String inputValueToText(String newValue)
return newValue;
以这种方式应用:
final TableColumn<Person,String> nameColumn = new TableColumn<Person,String>("Name");
nameColumn.setCellValueFactory(
cellDataFeatures -> cellDataFeatures.getValue().nameProperty());
nameColumn.setCellFactory(
cellDataFeatures -> new StringTableCell());
【讨论】:
对我来说这个解决方案不起作用。我有 2 列扩展了这个类,当我开始编辑一个列并尝试在同一行中编辑其他列时,我在线得到 ***Exception:getTableView().edit(getIndex(), getTableColumn());【参考方案4】:我很好奇,做了一些背景调查。
您正面临 JavaFX 中一个众所周知的错误问题。
背景
当你调用commitEdit(textField.getText())
时,它做的第一件事就是检查isEditing()
的值,如果是false
则返回,而不提交。
public void commitEdit(T newValue)
if (! isEditing()) return;
... // Rest of the things
为什么返回 false?
您可能已经发现,一旦您按下TAB
或ENTER
更改您的选择,就会调用cancelEdit()
将TableCell.isEditing()
设置为false。当调用 textField 的焦点属性侦听器内的 commitEdit()
时,isEditing()
已经返回 false。
解决方案/黑客
JavaFX 社区中一直在讨论该主题。那里的人已经发布了hacks,欢迎您查看。
TableView, TreeView, ListView - Clicking outside of the edited cell, node, or entry should commit the value TableCell - commit on focus lost not possible in every casea SO thread 中显示了一个 hack,它似乎可以完成工作,尽管我还没有尝试过。
【讨论】:
我认为这个错误现在已经修复了? docs.oracle.com/javafx/2/ui_controls/table-view.htm(“预期”一词后的代码)对我有用。 我发现了以下 hack,并且运行良好。所以我分享链接。它由 James D. 在 GitHub 上制作:gist.github.com/james-d/be5bbd6255a4640a5357 尝试使用 ControlsFX 中的 TableView2。这是一个更丰富的控件,更少的麻烦。以上是关于TableView 不会在焦点丢失事件上提交值的主要内容,如果未能解决你的问题,请参考以下文章
JavaScript / DOM:如果焦点丢失到另一个窗口(应用程序),如何防止模糊事件
Input 控件的Onchange 与onBlur 事件区别?
TextBox Binding TwoWay 直到焦点丢失 WP7 才会更新
Treeview 丢失焦点后依然高亮 SelectedNode
UITableView 永远不会在其委托上调用 tableView:shouldShowMenuForRowAtIndexPath: