javafx:一次切换多个CheckBoxTableCell-Checkbox

问题描述

我的目标

我有一个带有布尔列的可编辑表,在其中可以选中或取消选中包含的复选框和多行选择策略。我希望我的表格在我选中或取消选中某个复选框后立即自动切换所有选定行的所有复选框。 不知道你是否明白这一点: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 我该如何阻止呢?

解决方法

我认为最好直接通过模型/选择模型来完成,而不是通过单元工厂来完成。这是一种方法。基本思想是:

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

James' solution中所述,除了保留selectedItems的副本并使两者同步(在listChange / Listener api的约束下)的一种替代方法是将职责移至自定义复选框的动作处理程序中CheckBoxTableCell的扩展。

那也不是没有麻烦,因为checkBox本身不能直接访问。这可以通过一点技巧来克服(请注意:使用实现知识,因为我们期望checkBox作为单元格的图形!):侦听单元格的graphic属性,并在第一次其值不为null时注册该操作(并且再次取消注册属性侦听器。)

下面是有关如何使这种自​​定义单元可重复使用的示例:

  • 该单元配置有一个使用者(在TableCell上参数化),该使用者根据传入单元的状态执行实际的工作量
  • 在实例化时,单元将动作处理程序包装在使用者周围:该动作只是向消费者发送消息,并将单元作为参数传递
  • (技巧:在图形属性第一次失效时,将动作处理程序设置为复选框)

此处实现的行为与James的解决方案不同之处在于,它仅在激发选定行中的复选框时(而不是通过编程方式更改该属性时)才更新其他selectedItems的该项selected属性。使用哪个取决于要求。

自定义单元格:

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