Канонический способ отменить редактирование ячейки таблицы при сбое синтаксического анализа - PullRequest
0 голосов
/ 24 марта 2020

Редактировать :
Сначала я проголосовал за закрытие как дубликат после нахождения этого ответа от James_D, который устанавливает TextFormatter для TextField. Но потом, во-первых, я обнаружил, что (в контексте TableView) метод TextFieldTableCell.forTableColumn() фактически не рисует TextField, когда он начинает редактировать, а вместо этого LabeledText, который не подкласс TextInputControl, и поэтому setTextFormatter().
Во-вторых, я хотел что-то, что действовало бы знакомым образом. Возможно, я нашел «каноническое» решение в своем ответе: пусть другие судят.


Это TableColumn в TableView (все 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
        }
    }
})

Выдержка из класса 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
    }
    ...

Без начального редактирования Handler, когда я ввожу String, который не может быть проанализирован как Integer NumberFormatException, неудивительно, что выброшены. Но я также обнаружил, что число в ячейке устанавливается равным 0, что, скорее всего, не будет желаемым результатом.

Но вышесказанное кажется мне довольно неуклюжим решением.

Я посмотрел на ageCol и ageCol.cellFactory (так как они доступны внутри блока catch), но не мог видеть ничего лучшего и очевидного. Я также вижу, что можно легко получить Callback (ageCol.cellFactory), но для его вызова потребуется параметр cdf, то есть экземпляр CellDataFeatures, который опять-таки придется где-то хранить.

Я уверен, что с Swing был задействован некоторый механизм валидатора: то есть до того, как значение могло быть передано из компонента редактора (через некоторый делегат или что-то еще), было возможно переопределить некоторый механизм валидации. Но это IntegerStringConverter, кажется, функционирует как валидатор, хотя, кажется, не предоставляет никакого способа вернуться к существующему («старому») значению, если валидация не удалась.

Есть ли менее неуклюжий механизм, чем один, который я показал выше?

1 Ответ

1 голос
/ 25 марта 2020

Редактировать
Примечание улучшено после ценной информации Клеопатры.
Редактировать2
После полной проверки, лучше всего использовать существующий редактор по умолчанию и настроить это.


Я подумал, что приведу пример с LocalDate, немного более забавным, чем Integer. Имеется следующий класс:

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;
}

Затем вы создаете новый класс ячейки редактора, который точно такой же, как TextFieldTreeTableCell (подкласс TreeTableCell), который по умолчанию используется для создания редактора для ячейка таблицы TreeTableView. Однако вы не можете на самом деле создать подкласс TextFieldTreeTableCell, поскольку, например, его обязательное поле textField равно private.

Таким образом, вы полностью копируете код из источника * (всего около 30 строк) и вы называете это

class DueDateEditor extends TreeTableCell<Person, LocalDate> { 
    ...

Затем вам нужно создать новый класс StringConverter, подклассы LocalDateStringConverter. Причина подкласса заключается в том, что если вы этого не сделаете, то невозможно поймать DateTimeParseException, выданный fromString() при получении недопустимой даты: если вы используете LocalDateStringConverter, среда JavaFX, к сожалению, перехватывает ее, без каких-либо фреймов в трассировке стека с участием вашего собственного кода. Итак, вы делаете это:

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;
    }
}

Вернувшись в класс DueDateEditor, вы затем переписываете метод startEdit следующим образом. Обратите внимание: как и в случае с классом * 1037, textField фактически создается лениво при первом редактировании.

@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);
    }
}

Примечание: не пытайтесь искать CellUtils: это частный пакет, рассматриваемый пакет javafx.scene.control.cell.

Чтобы установить вещи до этого вы делаете:

Callback<TreeTableColumn, TreeTableCell> dueDateCellFactory =
        new Callback<TreeTableColumn, TreeTableCell>() {
            public TreeTableCell call(TreeTableColumn p) {
                return new DueDateEditor( new ValidatingLocalDateStringConverter() );
            }
        }
dueDateColumn.setCellFactory(dueDateCellFactory);

... в результате получается симпатичная, реактивная ячейка редактора: когда содержится недопустимая дата (допустимый шаблон yyyy-mm-dd; см. другой вариант LocalDate.parse() для других форматов) фон красный, в остальном нормальный. Ввод с правильной датой работает без проблем. Вы также можете ввести пустое значение String, которое будет возвращено как null LocalDate.

. С учетом вышеизложенного, нажмите Enter с недопустимой датой и установите дату null. Но переопределить вещи, чтобы предотвратить это (т.е. заставить вас ввести правильную дату или отменить редактирование, например, с помощью Escape), тривиально, используя поле ValidatingLocalDateStringConverter 'valid:

@Override
void commitEdit( LocalDate newDueDate ){
    if( getConverter().valid )
        super.commitEdit( newDueDate );
}

* Я не смог найти это онлайн. Я извлек из исходного файла javafx .jar javafx-controls-11.0.2-sources.jar

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...