TableView no confirma valores en el evento de pérdida de foco

Me gustaría crear una tabla con las siguientes características:

  • Editar en la tecla presionar
  • Tecla Intro = fila siguiente
  • Tecla tab = siguiente columna
  • Tecla de escape = cancelar editar

A continuación se muestra un código que implementa estas características. Los valores deben comprometerse en el foco perdido. Problema: no están comprometidos. El evento de cambio de enfoque se dispara, los valores serían correctos según la salida de la consola, pero al final los valores en las celdas de la tabla son los anteriores.

¿Alguien sabe cómo evitar esto y cómo se obtiene el objeto EditingCell actual para que pueda invocar el compromiso manualmente? Después de todo, debería haber algún tipo de verificador invocado que evite cambiar el foco si los valores no son correctos.

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 table = new TableView(); private final ObservableList 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, TableCell> cellFactory = (TableColumn p) -> new EditingCell(); TableColumn firstNameCol = new TableColumn("First Name"); TableColumn lastNameCol = new TableColumn("Last Name"); TableColumn emailCol = new TableColumn("Email"); firstNameCol.setMinWidth(100); firstNameCol.setCellValueFactory(new PropertyValueFactory("firstName")); firstNameCol.setCellFactory(cellFactory); firstNameCol.setOnEditCommit((CellEditEvent 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 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 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() { @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 { 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 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); } } } 

¡Muchas gracias!

Editar: lo he reducido. Parece que el problema es que el código JavaFX cancela el modo de edición cuando cambia el foco. Eso es malo.

 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)(WritableValue)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); } 

Tengo curiosidad e hice una investigación de antecedentes.

Te enfrentas al problema de un error conocido en JavaFX.

Fondo

Cuando llama a commitEdit(textField.getText()) , lo primero que hace es verificar el valor de isEditing() y devuelve si el valor es false , sin confirmar.

 public void commitEdit(T newValue) { if (! isEditing()) return; ... // Rest of the things } 

¿Por qué devuelve falso?

Como probablemente se haya cancelEdit() , tan pronto como presiona TAB o ENTER para cambiar su selección, se llama a cancelEdit() que establece el TableCell.isEditing() en falso. En el momento en que se commitEdit() dentro del detector de propiedad de foco de isEditing() , isEditing() ya está isEditing() falso .

Soluciones / Hacks

Ha habido una discusión continua sobre el tema en la comunidad JavaFX. La gente de allí ha publicado hacks , que son bienvenidos a mirar.

  • TableView, TreeView, ListView: al hacer clic fuera de la celda editada, el nodo o la entrada debe confirmar el valor
  • TableCell: compromiso en el foco perdido no es posible en todos los casos

Hay un truco que se muestra en un hilo SO , que parece hacer el trabajo, aunque todavía no lo he probado.

Mi propuesta para resolver esta atrocidad es la siguiente (perdón por JavaDoc).

Esta es una solución de redirección de cancelación para comprometer. Lo probé bajo LINUX con Java 1.8.0-121. Aquí, la única forma de descartar un editor de celda es presionar 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 extends TableCell { 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; } } 

Puede extender esta clase para admitir cualquier tipo de datos.

Ejemplo para un campo String es ( Person is an example bean):

 import javafx.scene.Node; import javafx.scene.control.TextField; import jfx.examples.tablebinding.PersonsModel.Person; public class StringTableCell extends AutoCommitTableCell { @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; } } 

Para ser aplicado de esta manera:

 final TableColumn nameColumn = new TableColumn("Name"); nameColumn.setCellValueFactory( cellDataFeatures -> cellDataFeatures.getValue().nameProperty()); nameColumn.setCellFactory( cellDataFeatures -> new StringTableCell()); 

Me encontré con el mismo problema y lo resolví combinando estos dos fragmentos de código:

Implementación personalizada de TableCell

 public class EditCell extends TableCell { 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 converter; /** * Creates and initializes an edit cell object. * * @param converter * the converter to convert from and to strings */ public EditCell(StringConverter 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 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 IDENTITY_CONVERTER = new StringConverter() { @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  EditCell createStringEditCell() { return new EditCell(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 table = getTableView(); if (table != null) { TableColumn column = getTableColumn(); CellEditEvent event = new CellEditEvent<>(table, new TablePosition(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 getNextColumn(boolean forward) { List> columns = new ArrayList<>(); for (TableColumn 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> getEditableColumns(TableColumn root) { List> 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 column : root.getColumns()) { columns.addAll(getEditableColumns(column)); } return columns; } } } 

Controlador

  @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 pos = table.getFocusModel().getFocusedCell() ; if (pos != null && event.getCode().isLetterKey()) { table.edit(pos.getRow(), pos.getTableColumn()); } }); } private  TableColumn createColumn(String title, Function property) { TableColumn col = new TableColumn<>(title); col.setCellValueFactory(cellData -> property.apply(cellData.getValue())); col.setCellFactory(column -> EditCell.createStringEditCell()); return col; } 

Encontré una solución simple que funciona en mi caso para TableCells. La idea es olvidarse de commitEdit at focus perdido. Deje que javafx haga su trabajo, y luego simplemente actualice el valor de la celda editada previamente.

 abstract class EditingTextCell extends TableCell { 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); ... } 

entonces, el único truco es implementar setItemValue () de tal manera que actualice la parte correcta del artículo.