javafx: одновременное переключение нескольких CheckBoxTableCell-Checkboxes

Моя цель

У меня есть редактируемая таблица с логическим столбцом, в котором можно установить или снять флажки, содержащие флажки, и политику выбора нескольких строк. Я хотел бы, чтобы моя таблица автоматически переключала все флажки всех выбранных строк, как только я устанавливаю или снимаю флажок. Не знаю, понимаете ли вы: D Я имею в виду:

  1. Я выбираю несколько строк
  2. Я устанавливаю или снимаю флажок одной из этих выбранных строк
  3. Флажки всех остальных строк автоматически устанавливаются или снимаются.

Теперь должно быть понятно ;)

Мой вопрос

(Я новичок в JavaFX! Я уже сделал то же самое, о чем прошу, с AWT/SWING, но не могу заставить его работать с JavaFX)

Есть ли подобное уже встроенное в JavaFX? Если нет, то как лучше всего добраться до моей цели?

Что я уже сделал

Я узнал, что вы можете прослушивать событие изменения, установив CheckBoxTableCell-Callback для CellFactory желаемого столбца. Я сделал это так:

TableColumn<FileSelection, Boolean> selectedColumn = new TableColumn<>("Sel");
selectedColumn.setCellValueFactory(new PropertyValueFactory<>("selected"));
selectedColumn.setCellFactory(CheckBoxTableCell.forTableColumn(rowidx -> {
    if (tblVideoFiles.getSelectionModel().isSelected(rowidx)) {
        tblVideoFiles.getSelectionModel().getSelectedItems().forEach(item -> {
            if (!item.getFile().equals(tblVideoFiles.getItems().get(rowidx).getFile())) {
                item.selectedProperty().set(!item.selectedProperty().get());
             }
         });
     }
     return fileList.get(rowidx).selectedProperty();
}));

Проблема здесь: как только флажок изменяется, он переключается сам, что приводит к циклу переключения проверки и снятия отметки: D Как я могу это остановить?


person Iamnino    schedule 21.08.2020    source источник
comment
минимальный воспроизводимый пример, пожалуйста.. ;)   -  person kleopatra    schedule 22.08.2020
comment
@kleopatra извините, вы правы. Я должен был предоставить Item-Class и, по крайней мере, структуру приложения с таблицей. На самом деле James_D предоставил все именно так, как я должен был сделать. Итак, вы считаете, что необходимо обновить мой вопрос? Если да, то делаю. :)   -  person Iamnino    schedule 24.08.2020


Ответы (2)


Я думаю, что это лучше всего делать напрямую через модель/модель выбора, а не через фабрику ячеек. Вот один подход. Основная идея такова:

  1. Создайте наблюдаемый список, который имеет extractor, сопоставленный со свойством элементов таблицы, представляющим, выбран ли этот элемент (в смысле флажка в таблице).
  2. Используйте прослушиватель для выбранных элементов таблицы, чтобы убедиться, что список, созданный на шаге 1, всегда содержит элементы, выбранные в таблице.
  3. Добавьте прослушиватель в список, созданный на шаге 1, который прослушивает обновления; то есть изменения в свойствах, представляющих, отмечены ли элементы с помощью флажка.
  4. Если изменено проверенное свойство элемента, выбранного в таблице, обновите все проверенные свойства выбранного элемента, чтобы они совпадали. Установите флаг, гарантирующий, что эти изменения будут игнорироваться слушателем.

Вот быстрый пример. Сначала простой класс модели таблицы:

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Item {
    private final StringProperty name = new SimpleStringProperty();
    private final BooleanProperty selected = new SimpleBooleanProperty();
    
    public Item(String name) {
        setName(name);
        setSelected(false);
    }
    
    public final StringProperty nameProperty() {
        return this.name;
    }
    
    public final String getName() {
        return this.nameProperty().get();
    }
    
    public final void setName(final String name) {
        this.nameProperty().set(name);
    }
    
    public final BooleanProperty selectedProperty() {
        return this.selected;
    }
    
    public final boolean isSelected() {
        return this.selectedProperty().get();
    }
    
    public final void setSelected(final boolean selected) {
        this.selectedProperty().set(selected);
    }
    
}

а затем пример приложения:

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage stage) {
        TableView<Item> table = new TableView<>();
        table.setEditable(true);

        TableColumn<Item, String> itemCol = new TableColumn<>("Item");
        itemCol.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
        table.getColumns().add(itemCol);

        TableColumn<Item, Boolean> selectedCol = new TableColumn<>("Select");
        selectedCol.setCellValueFactory(cellData -> cellData.getValue().selectedProperty());

        selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));

        table.getColumns().add(selectedCol);

        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

        // observable list of items that fires updates when the selectedProperty of
        // any item in the list changes:
        ObservableList<Item> selectionList = FXCollections
                .observableArrayList(item -> new Observable[] { item.selectedProperty() });

        // bind contents to items selected in the table:
        table.getSelectionModel().getSelectedItems().addListener(
                (Change<? extends Item> c) -> selectionList.setAll(table.getSelectionModel().getSelectedItems()));

        // add listener so that any updates in the selection list are propagated to all
        // elements:
        selectionList.addListener(new ListChangeListener<Item>() {

            private boolean processingChange = false;

            @Override
            public void onChanged(Change<? extends Item> c) {
                if (!processingChange) {
                    while (c.next()) {
                        if (c.wasUpdated() && c.getTo() - c.getFrom() == 1) {
                            boolean selectedVal = c.getList().get(c.getFrom()).isSelected();
                            processingChange = true;
                            table.getSelectionModel().getSelectedItems()
                                    .forEach(item -> item.setSelected(selectedVal));
                            processingChange = false;
                        }
                    }
                }
            }

        });

        for (int i = 1; i <= 20; i++) {
            table.getItems().add(new Item("Item " + i));
        }

        Scene scene = new Scene(new BorderPane(table));
        stage.setScene(scene);
        stage.show();
    }

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

}

Обратите внимание, что существует (раздражающее) правило, согласно которому наблюдаемые списки не должны изменяться во время обработки изменения (например, слушателем). Я не уверен, что это полностью подчиняется этому правилу, поскольку оно изменяет свойства наблюдаемого списка, которые являются частью экстрактора, в то время как слушатель обрабатывает эти изменения. Я думаю, что правило применяется только к добавлению/удалению элементов, а не к их обновлению, и этот код работает. Однако вам может понадобиться обходной путь, который заключает код, который обновляет выбранные элементы в Platform.runLater(...), чтобы обеспечить соответствие этому правилу:

        @Override
        public void onChanged(Change<? extends Item> c) {
            if (!processingChange) {
                while (c.next()) {
                    if (c.wasUpdated() && c.getTo() - c.getFrom() == 1) {
                        boolean selectedVal = c.getList().get(c.getFrom()).isSelected();
                        Platform.runLater(() -> {
                            processingChange = true;
                            table.getSelectionModel().getSelectedItems()
                                .forEach(item -> item.setSelected(selectedVal));
                            processingChange = false;
                        });
                    }
                }
            }
        }
person James_D    schedule 21.08.2020
comment
хм... правило применяется только к добавлению/удалению элементов хороший момент (не знаю, и спецификация не совсем ясна) Альтернативный подход может состоять в том, чтобы вообще не использовать список: вместо этого расширить CheckBoxTableCell и установите действие для флажка, которое синхронизирует selectedProperty всех selectedItems. Концептуально, это то, к чему относятся решения, IMO: делать что-то, когда установлен флажок, а не когда изменяется выбранное свойство элемента, который также содержится в selectedItems (хотя зависит от фактических требований, вопрос не совсем ясен; ) - person kleopatra; 22.08.2020
comment
мелькнула идея в ответе, просто пок :) - person kleopatra; 23.08.2020
comment
Хорошо, это очень помогло. Спасибо вам обоим. Я понял, и теперь понимаю, почему это работает с вашим кодом, но не с моим... На самом деле вы не используете список для заполнения таблицы. У вас есть только временный (наблюдаемый) список для установки выбранных элементов, пока я заполнял наблюдаемый список своими элементами, затем я передавал весь список в свою таблицу и, наконец, я пытался обновить список (и таблицу ), установив выбранные элементы, что является ненужной работой! Поэтому в конце концов я использовал прослушиватель событий, который лично я предпочитаю расширению CheckBoxTableCell :) - person Iamnino; 24.08.2020

Альтернатива сохранению копии selectedItems и синхронизации обоих (в условиях listChange/Listener API), как указано в решении Джеймса, заключается в том, чтобы передать ответственность обработчику действий checkBox пользовательского расширения CheckBoxTableCell.

Это тоже не без неприятностей, потому что сам checkBox недоступен напрямую. Это можно преодолеть с помощью небольшого трюка (осторожно: используя знания о реализации в том смысле, что мы ожидаем, что checkBox будет графическим изображением ячейки!): прослушайте графическое свойство ячейки и зарегистрируйте действие в первый раз, когда его значение не равно нулю (и снова отменить регистрацию прослушивателя свойства).

Ниже приведен пример того, как сделать такую ​​пользовательскую ячейку многоразовой:

  • ячейка настроена с потребителем (параметризованным в TableCell), который выполняет фактическую рабочую нагрузку в зависимости от состояния переданной ячейки.
  • при создании экземпляра ячейка оборачивает обработчик действия вокруг потребителя: действие просто сообщает потребителю, передавая ячейку в качестве параметра
  • (хитрость: при первом аннулировании графического свойства обработчику действия устанавливается флажок)

Реализованное здесь поведение отличается от решения Джеймса тем, что оно обновляет выбранное свойство элемента других selectedItems только в том случае, если срабатывает флажок в выбранной строке (а не при программном изменении свойства). Что использовать, зависит от требований.

Пользовательская ячейка:

public class ActionableCheckBoxTableCell<S, T> extends CheckBoxTableCell<S, T> {
    EventHandler<ActionEvent> action;
    InvalidationListener installer;
    
    public ActionableCheckBoxTableCell(Consumer<TableCell<S, T>> cellConsumer) {
        action = ev -> {
            cellConsumer.accept(this);
        };
        registerActionInstaller();
    }
    
    /**
     * Trick to set the action on checkBox (private in super).
     */
    protected void registerActionInstaller() {
        installer = ov -> {
            if (getGraphic() != null) {
                ((ButtonBase) getGraphic()).setOnAction(action);
                graphicProperty().removeListener(installer);
            }
        };
        graphicProperty().addListener(installer);
    }
}

Пример использования (захват решения Джеймса - просто замените проводку списка, установив фабрику пользовательских ячеек вместо фабрики основных ячеек):

Consumer<TableCell<Item, Boolean>> cellConsumer = cell -> {
    TableView<Item> tv = cell.getTableView();
    Item rowItem = tv.getItems().get(cell.getIndex());
    if (!tv.getSelectionModel().getSelectedItems().contains(rowItem)) return;
    tv.getSelectionModel().getSelectedItems().forEach(item -> item.setSelected(rowItem.isSelected()));
};
selectedCol.setCellFactory(cc -> new ActionableCheckBoxTableCell<>(cellConsumer));
person kleopatra    schedule 23.08.2020
comment
Вы также получаете голосование, так как ваше решение довольно умное. В конце концов я предпочел использовать прослушиватель, предложенный @James_D, просто потому, что я хочу изолировать инструменты и элементы, которые я только учусь использовать. Я работаю с таблицами и слушателями, и этого пока достаточно. :P Я займусь всеми графическими вещами, как только освоюсь с основами ;) - person Iamnino; 24.08.2020