问题
Edit:
I first voted to close as a duplicate after finding this answer by James_D, which sets a TextFormatter
on a TextField
. But then firstly I found that (in a TableView
context) the method TextFieldTableCell.forTableColumn()
does not in fact draw a TextField
when it starts editing, but instead a LabeledText
, which does not subclass TextInputControl
, and therefore does not have setTextFormatter()
.
Secondly, I wanted something which acted in a familiar sort of way. I may have produced the "canonical" solution in my answer: let others judge.
This is a TableColumn
in a TableView
(all Groovy):
TableColumn<Person, String> ageCol = new TableColumn("Age")
ageCol.cellValueFactory = { cdf -> cdf.value.ageProperty() }
int oldAgeValue
ageCol.onEditStart = new EventHandler(){
@Override
public void handle( Event event) {
oldAgeValue = event.oldValue
}
}
ageCol.cellFactory = TextFieldTableCell.forTableColumn(new IntegerStringConverter() {
@Override
public Integer fromString(String value) {
try {
return super.fromString(value)
}
catch ( NumberFormatException e) {
// inform user by some means...
println "string could not be parsed as integer..."
// ... and cancel the edit
return oldAgeValue
}
}
})
Excerpt from class Person:
public class Person {
private IntegerProperty age;
public void setAge(Integer value) { ageProperty().set(value) }
public Integer getAge() { return ageProperty().get() }
public IntegerProperty ageProperty() {
if (age == null) age = new SimpleIntegerProperty(this, "age")
return age
}
...
Without the start-edit Handler
, when I enter a String
which can't be parsed as an Integer
NumberFormatException
not surprisingly gets thrown. But I also find that the number in the cell then gets set to 0, which is likely not to be the desired outcome.
But the above strikes me as a pretty clunky solution.
I had a look at ageCol
, and ageCol.cellFactory
(as these are accessible from inside the catch
block) but couldn't see anything better and obvious. I can also see that one can easily obtain the Callback
(ageCol.cellFactory
), but calling it would require the parameter cdf
, i.e. the CellDataFeatures
instance, which again you'd have to store somewhere.
I'm sure a validator mechanism of some kind was involved with Swing: i.e. before a value could be transferred from the editor component (via some delegate or something), it was possible to override some validating mechanism. But this IntegerStringConverter
seems to function as a validator, although doesn't seem to provide any way to revert to the existing ("old") value if validation fails.
Is there a less clunky mechanism than the one I've shown above?
回答1:
Edit
NB improved after kleopatra's valuable insights.
Edit2
Overhauled completely after realising that the best thing is to use the existing default editor and tweak it.
I thought I'd give an example with a LocalDate
, slightly more fun than Integer
. Given the following class:
class Person(){
...
private ObjectProperty<LocalDate> dueDate;
public void setDueDate(LocalDate value) {
dueDateProperty().set(value);
}
public LocalDate getDueDate() {
return (LocalDate) dueDateProperty().get();
}
public ObjectProperty dueDateProperty() {
if (dueDate == null) dueDate = new SimpleObjectProperty(this, "dueDate");
return dueDate;
}
Then you create a new editor cell class, which is exactly the same as TextFieldTreeTableCell
(subclass of TreeTableCell
), which is used by default to create an editor for a TreeTableView
's table cell. However, you can't really subclass TextFieldTreeTableCell
as, for example, its essential field textField
is private
.
So you copy the code in full from the source* (only about 30 lines), and you call it
class DueDateEditor extends TreeTableCell<Person, LocalDate> {
...
You then have to create a new StringConverter
class, subclassing LocalDateStringConverter
. The reason for subclassing is that if you don't do that it is impossible to catch the DateTimeParseException
thrown by fromString()
when an invalid date is received: if you use LocalDateStringConverter
the JavaFX framework unfortunately catches it, without any frames in the stack trace involving your own code. So you do this:
class ValidatingLocalDateStringConverter extends LocalDateStringConverter {
boolean valid;
LocalDate fromString(String value) {
valid = true;
if (value.isBlank()) return null;
try {
return LocalDate.parse(value);
} catch (Exception e) {
valid = false;
}
return null;
}
}
Back in your DueDateEditor
class you then rewrite the startEdit
method as follows. NB, as with the TextFieldTreeTableCell
class, textField
is actually created lazily, when you first edit.
@Override
void startEdit() {
if (! isEditable()
|| ! getTreeTableView().isEditable()
|| ! getTableColumn().isEditable()) {
return;
}
super.startEdit();
if (isEditing()) {
if (textField == null) {
textField = CellUtils.createTextField(this, getConverter());
// this code added by me
ValidatingLocalDateStringConverter converter = getConverter();
Callable bindingFunc = new Callable(){
@Override
Object call() throws Exception {
// NB the return value from this is "captured" by the editor
converter.fromString( textField.getText() );
return converter.valid? '' : "-fx-background-color: red;";
}
}
def stringBinding = Bindings.createStringBinding( bindingFunc, textField.textProperty() );
textField.styleProperty().bind( stringBinding );
}
CellUtils.startEdit(this, getConverter(), null, null, textField);
}
}
NB don't bother trying to look up CellUtils
: this is package-private, the package in question being javafx.scene.control.cell.
To set things up you do this:
Callback<TreeTableColumn, TreeTableCell> dueDateCellFactory =
new Callback<TreeTableColumn, TreeTableCell>() {
public TreeTableCell call(TreeTableColumn p) {
return new DueDateEditor( new ValidatingLocalDateStringConverter() );
}
}
dueDateColumn.setCellFactory(dueDateCellFactory);
... the result is a nice, reactive editor cell: when containing an invalid date (acceptable pattern yyyy-mm-dd
; see other LocalDate.parse()
variant for other formats) the background is red, otherwise normal. Entering with a valid date works seamlessly. You can also enter an empty String
, which is returned as a null
LocalDate
.
With the above, pressing Enter with an invalid date sets the date to null
. But overriding things to prevent this happening (i.e. forcing you to enter a valid date, or cancel the edit, e.g. by Escape) is trivial, using the ValidatingLocalDateStringConverter
's valid
field:
@Override
void commitEdit( LocalDate newDueDate ){
if( getConverter().valid )
super.commitEdit( newDueDate );
}
* I couldn't find this online. I extracted from the javafx source .jar file javafx-controls-11.0.2-sources.jar
来源:https://stackoverflow.com/questions/60838502/canonical-way-to-cancel-a-table-cell-edit-if-parse-fails