在JavaFX中实现全选/反选复选框 Make a “Select All/Check All” check box in JavaFX

全选复选框是一个十分常见的需求。比如在邮件管理软件中,我们需要对邮件进行批量移动和删除。除了在列表中的每一个条目前添加一个复选框来展示这条邮件的选中状态,我们通常还会添加一个全选复选框。这个复选框可以起到两个作用:

  1. 点击复选框来切换所有子项的选中状态。
  2. 告知用户子项的选中状态为“全选”,“全未选”还是“部分选中”。
Gmail中的“全选复选框”和“子项复选框”

对应刚才说的两个作用,我们需要实现以下两个功能:

  1. 当复选框为“部分选中”或“全未选”时,点击后选中所有子项;当复选框为“全选”状态时,点击后取消选择所有子项。
  2. 在选择、取消选择以及添加、移除子项后,更新“全选”复选框的状态。

准备工作

为了演示如何实现这个功能,我们先来创建一个名叫Item的类,并添加一个名为select的BooleanProperty来记录每一项的选择情况。

public class Item { 
	private BooleanProperty selected = new SimpleBooleanProperty(false);

	public BooleaProperty selectedProperty() {
		return selected;
	}

	public boolean isSelected() {
		return selectedProperty().get();
	}

	public void setSelected(boolean value) {
		selectedProperty().set(value);
	}
}

然后我们新建一个ObservableList,将三个Item的实例加入到List中。在实际项目中,我们通常会直接使用TableView.setItems()或者TableView.itemsProperty().bind()来将这个列表显示在TableView中。

ObservableList<Item> list = FXCollection.observableArrayList();
Item item1 = new Item();
Item item2 = new Item();
Item item3 = new Item();
list.addAll(item1, item2, item3);
// tableView.itemsProperty().bind(list);

功能一:通过“全选复选框”修改子项

第一个功能可以通过给CheckAll复选框添加EventListener来实现。

CheckBox checkAll = new CheckBox();
checkAll.setOnAction(ae -> {
	if (checkAll.isSelected())
		list.forEach(item -> item.setSelected(true));
	else
		list.forEach(item -> item.setSelected(false));
});

在上面的代码中,我们通过setOnAction给这个CheckBox添加了一个ActionEventListener,即当这个CheckBox被点击时,就会触发这段代码,更新所有子项。

注意在这里checkAll.setOnAction()是在checkAll的值改变后才会触发,所以当改变后的值为true,我们将所有的子项的Selected的值也设为true,反之亦然。

功能二:子项变化时更新“全选复选框”

第二个功能需要我们监视所有子项的Selected的值,我们可以通过修改ObservableList的初始化方法,使得在任意一个子项的SelectedProperty变化时,都会触发ObservableList的ListChangeListener。

// 修改ObservableList的初始化方法。
ObservableList<Item> list = FXCollection.observableArrayList(item -> new Observable[] { item.selectedProperty() });
Item item1 = new Item();
Item item2 = new Item();
Item item3 = new Item();
list.addAll(item1, item2, item3);

然后再给list添加Listener就可以了。

ListChangeListener<Item> listener = change -> {
	boolean allTrue = true;
	boolean allFalse = true;
	for (Item item : change.getList()) {
		if (item.isSelected())
			allFalse = false;
		else
			allTrue = false;
	}

	if (allTrue) {
		checkAll.setSelected(true);
		checkAll.setIntermediate(false);
	} else if (allFalse) {
		checkAll.setSelected(false);
		checkAll.setIntermediate(false);
	} else {
		checkAll.setSelected(false);
		checkAll.setIntermediate(true);
	}
}

list.addListener(listener);

此时修改Item1,Item2,Item3的Selected的值,CheckAll的状态就会自动变化。

上面的代码还可以精简一下,虽然可读性降低了很多,但是逻辑没有变。

ListChangeListener<Item> listener = change -> {
	boolean allTrue = true, allFalse = true;
	for (Item item : change.getList()) {
		allTrue &= item.isSelected();
		allFalse &= !item.isSelected();
	}
	checkAll.setSelected(allTrue);
	checkAll.setIntermediate(allTrue == allFalse);
}

list.addListener(listener);

至此,一个基础的全选复选框就实现了。

优化

上面的例子虽然实现了功能,但是却有一个致命的问题,那就是点击“全选复选框”时,列表的Listener也会被调用,我们可以通过打印出Selected的值观察到这个问题。

ListChangeListener<Item> listener = change -> {
	boolean allTrue = true, allFalse = true;
	for (Item item : change.getList()) {
		System.out.println(item.isSelected());
		allTrue &= item.isSelected();
		allFalse &= !item.isSelected();
	}
	checkAll.setSelected(allTrue);
	checkAll.setIntermediate(allTrue == allFalse);
}

list.addListener(listener);

运行示例代码,点击“全选复选框”后,会看到以下的输出:

true
false
false
true
true
false
true
true
true

总共打印了9行。这是因为在点击“全选复选框”后,会逐个调用列表中的每一个Item的setSelected()方法。同时这又会触发列表上的ListChangeListener,对列表中的每一个值进行了遍历,3次每次3个总共9行。当列表中的Item数量达到1000个时,就会总共访问1000000次,这是不可接受的。我们要想办法不让这个Listener触发。

// 在控制器中添加类变量
private boolean bypass = false;

...

// 初始化组件
ListChangeListener<Item> listener = change -> {
	if (bypass)
		return;

	boolean allTrue = true, allFalse = true;
	for (Item item : change.getList()) {
		System.out.println(item.isSelected());
		allTrue &= item.isSelected();
		allFalse &= !item.isSelected();
	}
	checkAll.setSelected(allTrue);
	checkAll.setIntermediate(allTrue == allFalse);
}

list.addListener(listener);

checkAll.setOnAction(ae -> {
	bypass = true;
	if (checkAll.isSelected())
		list.forEach(item -> item.setSelected(true));
	else
		list.forEach(item -> item.setSelected(false));
	bypass = false;
});

重新执行程序并点击复选框,发现输出为空,说明Listener中的逻辑被成功跳过。

示例:在TableView实现“全选复选框”

正如文章一开始的图片所显示,我们通常会在表格的第一列显示复选框,标题行显示“全选复选框”,在JavaFX中,我们需要用TableView来实现这个功能。

TableView<Item> table = new TableView<>(list);
TableColumn<Item, boolean> col1 = new TableColumn<>();
tableView.getColumns().add(col1);

col1.setGraphic(checkAll);
col1.setCellValueFactory(new PropertyValueFactory<>("selected"));
col1.setCellFactory(CheckBoxTableCell.forTableColumn(index -> table.getItems().get(index).selectedProperty()));

发表评论

您的电子邮箱地址不会被公开。