javafx:一次切换多个 CheckBoxTableCell-Checkboxes

javafx: Toggle multiple CheckBoxTableCell-Checkboxes at once

我的目标

我有一个带有布尔列的 editable table,可以在其中选中或取消选中包含的复选框和多行 selection 策略。我希望我的 table 在我选中或取消选中一个时自动切换所有 selected 行的所有复选框。 不知道你是否明白我的意思:D 我的意思是:

  1. 我select多行
  2. 我选中或取消选中那些 selected 行之一的复选框
  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 我该如何阻止它?

我认为这最好直接通过 model/selection 模型完成,而不是通过细胞工厂。这是一种方法。基本思路是:

  1. 创建一个可观察列表,其中有一个 提取器 映射到 table 项目的 属性 表示该项目是否被选中(在table)
  2. 中复选框的意义
  3. 对 table 的选定项目使用侦听器以确保在步骤 1 中创建的列表始终包含在 table
  4. 中选定的项目
  5. 向步骤 1 中创建的列表添加一个侦听器以侦听更新;即表示是否通过复选框检查项目的属性的变化。
  6. 如果 table-selected 项目的选中 属性 发生变化,请更新所有选中项目的选中属性以匹配。设置一个标志,确保监听器忽略这些更改。

这是一个简单的例子。首先是一个简单的 table 模型 class:

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

}

请注意,有一个(烦人的)规则,即在处理更改时(例如,由侦听器)不应更改可观察列表。我不确定这是否完全遵守该规则,因为它更改了作为提取器一部分的可观察列表的属性,而侦听器正在处理这些更改。我 认为 该规则仅适用于 adding/removing 元素,不适用于更新它们,并且此代码似乎有效。但是,您可能需要 hack-around 将更新所选项目的代码包装在 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;
                        });
                    }
                }
            }
        }

所述,保留 selectedItems 的副本并同步两者(在 listChange/Listener api 的限制下)的替代方法是将责任转移到CheckBoxTableCell 的自定义扩展的复选框的操作处理程序。

这也不是没有麻烦,因为复选框本身不能直接访问。这可以通过一个小技巧来克服(注意:使用实现知识,因为我们希望复选框作为单元格的图形!):听取单元格的图形 属性 并在其值为第一次时注册操作不为空(并且 de-register 又是 属性 侦听器)。

下面是有关如何制作此类自定义单元格的示例re-usable:

  • 该单元配置了一个消费者(在 TableCell 上参数化),该消费者根据 passed-in 单元的状态执行实际工作负载
  • 在实例化时,单元格围绕消费者包装一个动作处理程序:动作只是向消费者发送消息,将单元格作为参数传递
  • (诀窍:在图形第一次失效时 属性,操作处理程序设置为复选框)

此处实现的行为与 James 的解决方案不同,因为如果选定行中的复选框触发(而不是在以编程方式更改 属性).使用哪个取决于需求。

自定义单元格:

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

使用示例(劫持 James 的解决方案 - 只需通过设置自定义单元工厂而不是核心单元工厂来替换列表布线):

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