【问题标题】:javafx: Toggle multiple CheckBoxTableCell-Checkboxes at oncejavafx:一次切换多个 CheckBoxTableCell-Checkboxes
【发布时间】:2020-12-10 22:47:55
【问题描述】:

我的目标

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

【问题讨论】:

  • @kleopatra 对不起,你是对的。我应该已经提供了 Item-Class 以及至少带有表格的应用程序结构。实际上,James_D 提供了我应该做的一切。那么您认为有必要更新我的问题吗?如果是,我会这样做。 :)

标签: javafx checkbox tablecolumn


【解决方案1】:

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

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

}

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

【讨论】:

  • hmm .. 规则仅适用于添加/删除元素 好点(不知道,规范也不是很清楚)另一种方法可能是不使用完全列出:而是扩展 CheckBoxTableCell 并为复选框设置一个操作,以同步所有 selectedItems 的 selectedProperty。从概念上讲,这将是解决方案所属的地方,IMO:单击复选框时执行某些操作,而当也包含在 selectedItems 中的项目的选定属性发生更改时(尽管取决于实际要求,问题并不完全清楚; )
  • 在答案中闪现了这个想法,只是一个 poc :)
  • 好的,这很有帮助。感谢你们俩。我明白了,现在明白为什么它适用于您的代码但不适用于我的代码......实际上您没有使用列表来填写表格。您只有一个临时(可观察)列表来设置“选定”项目,而我正在用我的项目填充可观察列表,然后我将整个列表传递给我的表,最后我试图更新列表(和表)通过设置我的项目“选择”,这都是不必要的工作!所以最终我使用了事件监听器,我个人更喜欢扩展 CheckBoxTableCell :)
【解决方案2】:

James' solution 中所述,保留 selectedItems 的副本并同步两者(在 listChange/Listener api 的约束中)的另一种方法是将责任转移到自定义扩展的 checkBox 的操作处理程序中CheckBoxTableCell。

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

下面是一个示例,说明如何使此类自定义单元格可重复使用:

  • 单元配置有一个消费者(在 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));

【讨论】:

  • 您也获得了赞成票,因为您的解决方案非常聪明。最终我更喜欢使用@James_D 提出的监听器,只是因为我想隔离我刚刚学习使用的工具和项目。我正在与表和听众一起工作,现在这已经足够了。 :P 只要我掌握了基础知识,我就会研究所有的图形内容;)
猜你喜欢
  • 2015-12-07
  • 1970-01-01
  • 1970-01-01
  • 2021-11-08
  • 2021-08-02
  • 1970-01-01
  • 2018-04-06
  • 1970-01-01
  • 2011-07-21
相关资源
最近更新 更多