Использование setRowFactory для стилизации строк не работает с видимыми строками (JavaFX 11) - PullRequest
2 голосов
/ 09 марта 2020

У меня есть TableView, который обновляется из ObservableList. У него есть две колонки. Когда файл загружен, список заполняется, и таблица обновляется (первоначально заполняется только первый столбец). После проверки элементов в списке второй столбец заполняется флагом успеха или неудачи. Используя setRowFactory, я обновляю фоновый стиль строки: зеленый для успеха или красный для неудачи. Некоторые элементы не проходят проверку и имеют стиль "". Таблица содержит около десятка строк, видимых из нескольких тысяч строк. У меня проблема в том, что видимые строки не обновляют свой стиль фона, пока они не прокручиваются из поля зрения, а затем снова возвращаются.

Я смог преодолеть это с помощью refre таблицы sh (), но это вызывает другую проблему. Первый столбец доступен для редактирования, чтобы можно было исправить данные перед повторной проверкой. Если используется метод refre sh (), он лишает возможности редактировать ячейку. Текстовое поле все еще появляется, но отключено (без рамки фокуса и без возможности выделять или редактировать его содержимое).

Если я опускаю метод refre sh (), редактирование работает просто отлично. Включите refre sh (), и таблица будет отображаться правильно без необходимости прокрутки, но редактирование будет прервано.

Так что я могу иметь либо редактируемые ячейки, либо отображаемые строки, но не оба. Помимо этой проблемы код работает нормально. Я прочитал бесчисленные примеры и проблемы TableView, и связанные решения, и ничто из того, что я пробовал, не устранило проблему. В моих усилиях я вижу, что переопределенный метод updateItem вызывается только когда строка перерисовывается после того, как снова становится видимой. Я думаю, что мне нужен другой механизм для стилизации строк при изменении validationResponse, но это то, где я застреваю.

Так что мой вопрос заключается в том, как сделать так, чтобы видимые строки таблицы обновляли свой стиль без прокрутки, не прерывая работу редактирование ячейки? Спасибо !!

Редактировать:

Ниже приведен пример воспроизводимого кода. Нажмите первую кнопку, чтобы заполнить таблицу исходными данными. Нажмите вторую кнопку для имитации проверки. Второй столбец будет обновлен с ответом проверки, но стилизация не вступит в силу, пока строки не прокручиваются из вида, а затем обратно для просмотра. На этом этапе первый столбец доступен для редактирования. Если вы раскомментируете строку tblGCode.refre sh () и повторно запустите тест, стилирование будет применено немедленно без прокрутки, но редактирование ячейки в первом столбце больше не работает.

Основной класс:

public class TableViewTest extends Application {

    private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
        item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});
    private final TableView tblGCode = new TableView();

    @Override
    public void start(Stage stage) {

        TableColumn<GCodeItem, String> colGCode = new TableColumn<>("GCode");
        colGCode.setCellValueFactory(new PropertyValueFactory<>("gcode"));
        TableColumn<GCodeItem, String> colStatus = new TableColumn<>("Status");
        colStatus.setCellValueFactory(new PropertyValueFactory<>("validationResponse"));

        // Set first column to be editable
        tblGCode.setEditable(true);
        colGCode.setEditable(true);
        colGCode.setCellFactory(TextFieldTableCell.forTableColumn());
        colGCode.setOnEditCommit((TableColumn.CellEditEvent<GCodeItem, String> t) -> {
            ((GCodeItem) t.getTableView().getItems().get(t.getTablePosition().getRow())).setGcode(t.getNewValue());
        });

        // Set row factory
        tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {
            private final Tooltip tip = new Tooltip();
            {
                tip.setShowDelay(new Duration(250));
            }

            @Override
            protected void updateItem(GCodeItem item, boolean empty) {
                super.updateItem(item, empty);

                if(item == null || empty) {
                    setStyle("");
                    setTooltip(null);
                } else {
                    if(item.isValidated()) {
                        if(item.hasError()) {
                            setStyle("-fx-background-color: #ffcccc"); // red
                            tip.setText(item.getErrorDescription());
                            setTooltip(tip);
                        } else {
                            setStyle("-fx-background-color: #ccffdd"); // green
                            setTooltip(null);
                        }
                    } else {
                        setStyle("");                                
                        setTooltip(null);
                    }
                }
                //tblGCode.refresh(); // this works to give desired styling, but breaks editing
            }
        });

        tblGCode.getColumns().setAll(colGCode, colStatus);
        tblGCode.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

        // buttons to simulate issue
        Button btnPopulate = new Button("1. Populate Table");
        btnPopulate.setOnAction(eh -> populateTable());
        Button btnValidate = new Button("2. Validate Table");
        btnValidate.setOnAction(eh -> simulateValidation());

        var scene = new Scene(new VBox(tblGCode, btnPopulate, btnValidate), 640, 320);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

    private void populateTable() {
        // simulates updating of ObservableList with first couple of dozen lines of a file
        gcodeItems.add(new GCodeItem("(1001)"));
        gcodeItems.add(new GCodeItem("(T4  D=0.25 CR=0 - ZMIN=-0.4824 - flat end mill)"));
        gcodeItems.add(new GCodeItem("G90 G94"));
        gcodeItems.add(new GCodeItem("G17"));
        gcodeItems.add(new GCodeItem("G20"));
        gcodeItems.add(new GCodeItem("G28 G91 Z0"));
        gcodeItems.add(new GCodeItem("G90"));
        gcodeItems.add(new GCodeItem(""));
        gcodeItems.add(new GCodeItem("(Face1)"));
        gcodeItems.add(new GCodeItem("T4 M6"));
        gcodeItems.add(new GCodeItem("S5000 M3"));
        gcodeItems.add(new GCodeItem("G54"));
        gcodeItems.add(new GCodeItem("M8"));
        gcodeItems.add(new GCodeItem("G0 X1.3842 Y-1.1452"));
        gcodeItems.add(new GCodeItem("Z0.6"));
        gcodeItems.add(new GCodeItem("Z0.2"));
        gcodeItems.add(new GCodeItem("G1 Z0.015 F20"));
        gcodeItems.add(new GCodeItem("G18 G3 X1.3592 Z-0.01 I-0.025 K0"));
        gcodeItems.add(new GCodeItem("G1 X1.2492"));
        gcodeItems.add(new GCodeItem("X-1.2492 F40"));
        gcodeItems.add(new GCodeItem("X-1.25"));
        gcodeItems.add(new GCodeItem("G17 G2 X-1.25 Y-0.9178 I0 J0.1137"));
        gcodeItems.add(new GCodeItem("G1 X1.25"));
        gcodeItems.add(new GCodeItem("G3 X1.25 Y-0.6904 I0 J0.1137"));

        // Add list to table
        tblGCode.setItems(gcodeItems);
    }

    private void simulateValidation() {
        // sets validationResponse on certain rows (not every row is validated)
        gcodeItems.get(2).setValidationResponse("ok");
        gcodeItems.get(3).setValidationResponse("ok");
        gcodeItems.get(4).setValidationResponse("ok");
        gcodeItems.get(5).setValidationResponse("ok");
        gcodeItems.get(6).setValidationResponse("ok");
        gcodeItems.get(9).setValidationResponse("error:20");
        gcodeItems.get(10).setValidationResponse("ok");
        gcodeItems.get(11).setValidationResponse("ok");
        gcodeItems.get(12).setValidationResponse("ok");
        gcodeItems.get(13).setValidationResponse("ok");
        gcodeItems.get(14).setValidationResponse("ok");
        gcodeItems.get(15).setValidationResponse("ok");
        gcodeItems.get(16).setValidationResponse("ok");
        gcodeItems.get(17).setValidationResponse("ok");
        gcodeItems.get(18).setValidationResponse("ok");
        gcodeItems.get(19).setValidationResponse("ok");
        gcodeItems.get(20).setValidationResponse("ok");
        gcodeItems.get(21).setValidationResponse("ok");
        gcodeItems.get(22).setValidationResponse("ok");
        gcodeItems.get(23).setValidationResponse("ok");
    }
}

Модель GCodeItem:

public class GCodeItem {

    private final SimpleStringProperty gcode;
    private final SimpleStringProperty validationResponse;
    private ReadOnlyBooleanWrapper validated;
    private ReadOnlyBooleanWrapper hasError;
    private ReadOnlyIntegerWrapper errorNumber;
    private ReadOnlyStringWrapper errorDescription;

    public GCodeItem(String gcode) {
        this.gcode = new SimpleStringProperty(gcode);
        this.validationResponse = new SimpleStringProperty("");
        this.validated = new ReadOnlyBooleanWrapper();
        this.hasError = new ReadOnlyBooleanWrapper();
        this.errorNumber = new ReadOnlyIntegerWrapper();
        this.errorDescription = new ReadOnlyStringWrapper();

        validated.bind(Bindings.createBooleanBinding(
            () -> ! "".equals(getValidationResponse()),
            validationResponse
        ));

        hasError.bind(Bindings.createBooleanBinding(
            () -> ! ("ok".equals(getValidationResponse()) ||
                    "".equals(getValidationResponse())),
            validationResponse
        ));

        errorNumber.bind(Bindings.createIntegerBinding(
            () -> {
                String vResp = getValidationResponse();
                if ("ok".equals(vResp)) {
                    return 0;
                } else {
                    // should handle potential exceptions here...
                    if(vResp.contains(":")) {
                        int en = Integer.parseInt(vResp.split(":")[1]);
                        return en ;
                    } else {
                        return 0;
                    }
                }
            }, validationResponse
        ));

        errorDescription.bind(Bindings.createStringBinding(
            () -> {
                int en = getErrorNumber() ;
                return GrblDictionary.getErrorDescription(en);
            }, errorNumber
        ));
    }

    public final String getGcode() {
        return gcode.get();
    }
    public final void setGcode(String value) {
        gcode.set(value);
    }
    public SimpleStringProperty gcodeProperty() {
        return this.gcode;
    }

    public final String getValidationResponse() {
        return validationResponse.get();
    }
    public final void setValidationResponse(String value) {
        validationResponse.set(value);
    }
    public SimpleStringProperty validationResponseProperty() {
        return this.validationResponse;
    }

    public Boolean isValidated() {
        return validatedProperty().get();
    }
    public ReadOnlyBooleanProperty validatedProperty() {
        return validated.getReadOnlyProperty();
    }

    // ugly method name to conform to method naming pattern:
    public final boolean isHasError() {
        return hasErrorProperty().get();
    }
    // better method name:
    public final boolean hasError() {
        return isHasError();
    }
    public ReadOnlyBooleanProperty hasErrorProperty() {
        return hasError.getReadOnlyProperty();
    }

    public final int getErrorNumber() {
        return errorNumberProperty().get();
    }
    public ReadOnlyIntegerProperty errorNumberProperty() {
        return errorNumber.getReadOnlyProperty() ;
    }

    public final String getErrorDescription() {
        return errorDescriptionProperty().get();
    }
    public ReadOnlyStringProperty errorDescriptionProperty() {
        return errorDescription.getReadOnlyProperty();
    }
}

Поддерживающий класс словаря (сокращенный):

public class GrblDictionary {

    private static final Map<Integer, String> ERRORS = Map.ofEntries(
        entry(1, "G-code words consist of a letter and a value. Letter was not found."),
        entry(2, "Numeric value format is not valid or missing an expected value."),
        entry(17, "Laser mode requires PWM outentry."),
        entry(20, "Unsupported or invalid g-code command found in block."),
        entry(21, "More than one g-code command from same modal group found in block."),
        entry(22, "Feed rate has not yet been set or is undefined.")
    );

    public static String getErrorDescription(int errorNumber) {
        return ERRORS.containsKey(errorNumber) ? ERRORS.get(errorNumber) : "Unrecognized error number.";
    }
}

Правка # 2:

Если я заменю код TableView.setRowFactory на TableColumn.setCellFactory, как показано ниже, я получу желаемый эффект, и редактирование все еще работает. Это разумное решение, или я действительно должен использовать setRowFactory и правильно распознавать изменения списка setRowFactory? В моем тестировании казалось, что переопределенный метод updateItem вызывался только при прокрутке строк для просмотра.

colStatus.setCellFactory(tc -> new TableCell<GCodeItem, String>() {
    private final Tooltip tip = new Tooltip();
    {
        tip.setShowDelay(new Duration(250));
    }

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);

        TableRow<GCodeItem> row = this.getTableRow();
        GCodeItem rowItem = row.getItem();

        if(item == null || empty) {
            row.setStyle("");
            row.setTooltip(null);
        } else {
            if(rowItem.isValidated()) {
                if(rowItem.hasError()) {
                    row.setStyle("-fx-background-color: #ffcccc"); // red
                    tip.setText(rowItem.getErrorDescription());
                    row.setTooltip(tip);
                } else {
                    row.setStyle("-fx-background-color: #ccffdd"); // green
                    row.setTooltip(null);
                }
            } else {
                row.setStyle("");                                
                row.setTooltip(null);
            }
            setText(item);
        }
    }
});

Edit # 3:

Большое спасибо Клеопатра и Джеймс_Д У меня сейчас есть решение. Переопределение isItemChanged() на фабрике строк решило мою проблему.

Ответы [ 2 ]

2 голосов
/ 10 марта 2020

Место установки условного стиля строки - это пользовательский TableRow - больше нигде. Как всегда, содержащиеся узлы - например, tableCells здесь - не должны вмешиваться в состояние своего родителя, никогда!

Основная проблема с таким стилем в tableRow заключается в том, что row.updateItem(...) не вызывается, когда мы можем ожидать его, в частности, не после обновления свойства. Есть два варианта решения (кроме того, чтобы убедиться, что таблица вообще уведомлена об обновлениях свойств, не показанных в столбцах, с использованием экстрактора, как уже предложено Джеймс )

Быстрый опция заключается в том, чтобы принудительно всегда обновлять принудительно, переопределяя isItemChanged:

@Override
protected boolean isItemChanged(GCodeItem oldItem,
        GCodeItem newItem) {
    return true;
}

Другой вариант - обновлять стили как в updateItem(...), так и updateIndex(...) (последний вызывается всегда, когда что-либо происходит в данных )

@Override
protected void updateIndex(int i) {
    super.updateIndex(i);
    doUpdateItem(getItem());
}

@Override
protected void updateItem(CustomItem item, boolean empty) {
    super.updateItem(item, empty);
    doUpdateItem(item);
}

protected void doUpdateItem(CustomItem item) {
    // actually do the update and styling
}

Выбор между обоими зависит от контекста и требований. Видел контексты, в которых тот или другой не работал должным образом, без четкого указания, когда / почему это произошло (слишком ленив, чтобы действительно копать;)


В стороне - пара комментариев к вопросу, которые со временем значительно улучшилось, но все еще не совсем [MCVE]:

  • элемент данных слишком сложен (для базового стиля c нет необходимости в нескольких прямых / косвенных переплетенных условиях) и недостаточно полные, чтобы действительно продемонстрировать требования (например, обновить после редактирования значения, определяющего условие ошибки)
  • элемент данных предоставляет свойства (хорошая вещь!) - так что используйте их (против PropertyValueFactory, плохая вещь! )
  • с записываемым свойством, пользовательский обработчик редактирования редактирования не требуется
  • TableColumn редактируется по умолчанию, что делает col.setEditable(true) недоступным. Если только некоторые столбцы должны редактироваться, остальные должны быть установлены в false
2 голосов
/ 09 марта 2020

Основная проблема c заключается в том, что таблица не форсирует обновления строки таблицы при изменении соответствующих свойств. Использование «экстрактора», как вы делаете с

private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList(
    item -> new Observable[]{item.validatedProperty(), item.errorDescriptionProperty()});

, должно работать , но, похоже, таблица не вызывает принудительное обновление строк, когда базовый список данных запускает изменения типа updated. (Я бы посчитал это ошибкой; возможно, команда JavaFX просто не считает эту функцию поддерживаемой.)

Один из подходов здесь заключается в том, чтобы TableRow зарегистрировал слушателя с текущим элементом * 1010. * (или любое другое желаемое свойство) и обновите строку при ее изменении. Здесь нужно немного позаботиться, потому что текущий элемент, отображаемый в строке, может измениться (например, при прокрутке или при изменении данных в списке), поэтому необходимо соблюдать itemProperty() и убедиться, что слушатель зарегистрирован со свойством в правильный пункт. Это выглядит так:

    // Set row factory
    tblGCode.setRowFactory(tbl -> new TableRow<GCodeItem>() {

        private final Tooltip tip = new Tooltip();

        private final ChangeListener<String> listener = (obs, oldValidationResponse, newValidationResponse) -> 
            updateStyleAndTooltip();

        {
            tip.setShowDelay(new Duration(250));
            itemProperty().addListener((obs, oldItem, newItem) -> {
                if (oldItem != null) {
                    oldItem.validationResponseProperty().removeListener(listener);
                }
                if (newItem != null) {
                    newItem.validationResponseProperty().addListener(listener);
                }
                updateStyleAndTooltip();
            });
        }

        @Override
        protected void updateItem(GCodeItem item, boolean empty) {
            super.updateItem(item, empty);
            updateStyleAndTooltip();

        }

        private void updateStyleAndTooltip() {
            GCodeItem item = getItem();
            if(item == null || isEmpty()) {
                setStyle("");
                setTooltip(null);
            } else {
                if(item.isValidated()) {
                    if(item.hasError()) {
                        setStyle("-fx-background-color: #ffcccc"); // red
                        tip.setText(item.getErrorDescription());
                        setTooltip(tip);
                    } else {
                        setStyle("-fx-background-color: #ccffdd"); // green
                        setTooltip(null);
                    }
                } else {
                    setStyle("");                                
                    setTooltip(null);
                }
            }   
        }
    });

Обратите внимание, что теперь вам больше не нужен список, созданный с помощью экстрактора:

private final ObservableList<GCodeItem> gcodeItems = FXCollections.observableArrayList();

, и действительно, это будет работать без реализации зависимых свойств как JavaFX (bound ) свойства (при условии, что они согласуются с другими данными); хотя я по-прежнему считаю версию, которую вы в настоящее время должны использовать лучше.

Кстати, вкратце, ваш стиль будет работать лучше, если вы будете использовать -fx-background вместо -fx-background-color. По умолчанию цвет фона (-fx-background-color) строки устанавливается равным -fx-background. Однако цвет текста зависит от -fx-background: если -fx-background светлый, то используется темный текст, и наоборот. По умолчанию при выборе строки изменяется -fx-background, что приводит к изменению цвета текста, поэтому в вашей реализации вы заметите, что текст трудно прочитать в выбранной строке (проверено или с ошибкой). Короче говоря, изменение -fx-background будет играть лучше с выделением, чем изменение -fx-background-color.

...