JavaFx绑定:有没有办法将值绑定到可观察列表



我有一个表视图,它的内容是一个包含数字的ObservableList,我还有一个文本字段,应该在表中显示这些值的总和。有什么方法可以将这个文本字段绑定到数字属性的和吗?

注意:用户可以编辑此列表中的值,可以添加更多元素,也可以删除一些元素。我如何使用JavaFx绑定正确地绑定这些数字的总和,而不是用旧的方式迭代列表并手动求和,每次更改都会再次重复?

如果(且仅当)使用提取器创建列表,ObservableList将触发更新事件。提取器是将列表的每个元素映射到Observable的数组的函数;如果这些Observable中的任何一个更改了其值,则该列表将触发相应的更新事件并变为无效。

所以这里的两个步骤是:

  1. 使用提取器创建列表
  2. 创建一个绑定,该绑定在列表无效时计算总数

因此,如果您的表有一个模型类,例如:

public class Item {
private final IntegerProperty value = new SimpleIntegerProperty();
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
// other properties, etc...
}

然后用创建表格

TableView<Item> table = new TableView<>();
table.setItems(FXCollections.observableArrayList(item -> 
new Observable[] { item.valueProperty() }));

现在您可以使用创建绑定

IntegerBinding total = Bindings.createIntegerBinding(() ->
table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
table.getItems());

实现说明:上面createIntegerBinding的两个参数是一个计算int值和要观察的任何值的函数。如果任何观测值(这里只有一个,table.getItems())无效,则重新计算该值。请记住,我们创建了table.getItems(),因此如果项目的任何valueProperty()发生更改,它将无效。作为第一个参数的函数使用lambda表达式和Java 8 Streams API,它大致相当于

() -> {
int totalValue = 0 ;
for (Item item : table.getItems()) {
totalValue = totalValue + item.getValue();
}
return totalValue ;
}

最后,如果你想要一个标签来显示总数,你可以做一些类似的事情

Label totalLabel = new Label();
totalLabel.textProperty().bind(Bindings.format("Total: %d", total));

这里有一个SSCCE:

import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
import javafx.util.converter.NumberStringConverter;
public class TotallingTableView extends Application {
@Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.getColumns().add(
column("Item", Item::nameProperty, new DefaultStringConverter()));
table.getColumns().add(
column("Value", Item::valueProperty, new NumberStringConverter()));
table.setItems(FXCollections.observableArrayList(
item -> new Observable[] {item.valueProperty() }));
IntStream.rangeClosed(1, 20)
.mapToObj(i -> new Item("Item "+i, i))
.forEach(table.getItems()::add);
IntegerBinding total = Bindings.createIntegerBinding(() -> 
table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
table.getItems());
Label totalLabel = new Label();
totalLabel.textProperty().bind(Bindings.format("Total: %d", total));
Button add = new Button("Add item");
add.setOnAction(e -> 
table.getItems().add(new Item("New Item", table.getItems().size() + 1)));
Button remove = new Button("Remove");
remove.disableProperty().bind(
Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));
remove.setOnAction(e -> 
table.getItems().remove(table.getSelectionModel().getSelectedItem()));
HBox buttons = new HBox(5, add, remove);
buttons.setAlignment(Pos.CENTER);
VBox controls = new VBox(5, totalLabel, buttons);
VBox.setVgrow(totalLabel, Priority.ALWAYS);
totalLabel.setMaxWidth(Double.MAX_VALUE);
totalLabel.setAlignment(Pos.CENTER_RIGHT);
BorderPane root = new BorderPane(table, null, null, controls, null);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String title, 
Function<S, ObservableValue<T>> property, StringConverter<T> converter) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(TextFieldTableCell.forTableColumn(converter));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}

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

请注意,这不是最高效的实现,而是(IMHO)保持代码最干净的实现。如果表中有大量项目,那么通过迭代并求和来从头开始重新计算总数可能会非常昂贵。另一种方法是侦听对列表的添加/删除更改。添加项时,将其值添加到总数中,并使用value属性注册侦听器,如果值发生更改,该侦听器将更新总数。从列表中删除某个项时,请从value属性中删除侦听器,并从总数中减去该值。这避免了不断从头开始重新计算,但代码更难破译。

我只需要在ObservableList中添加一个监听器,并让它在任何更改时更新标签。

list.addListener((obs, oldValue, newValue) -> {  
textfield.setText(list.stream().mapToInt(x -> x.intValue()).sum(); 
});

类似的东西应该可以

最新更新