JavaFX 中 ListView 的问题

1 java javafx

我一直在练习提高 JavaFX 的技能,但在使用 JavaFX 时遇到了一些问题ListView。我制作了一个自定义布局,其中包含标签、充当图像持有者的圆圈和复选框,如下所示。

屏幕截图

然后我创建了一个ListCell类类型为“formation”的类。类类型包含标签的字符串、圆圈的图像和用于记录复选框状态的布尔属性。我尝试了多种解决方案,但我仍然面临问题。

第一个解决方案是将复选框绑定到复选框的布尔属性。问题是,当选中或取消选中单个复选框时,所有复选框都会被选中/取消选中。

我还尝试了复选框的选择侦听器,但是当以编程方式设置复选框的初始状态时,ListView它会干扰侦听器(不断触发侦听器并进行未调用的更改)。

我想要的是对特定复选框进行的更改不影响所有复选框,并使复选框选择侦听器仅响应用户生成的点击(通过单击检查复选框)而不响应编程更改(例如 checkbox.setselected(true))

这是以下代码ListCell

package Formation.cells;

import Formation.Formation;
import com.jfoenix.controls.JFXCheckBox;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.embed.swing.SwingFXUtils;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

import java.awt.image.BufferedImage;
import java.util.concurrent.atomic.AtomicBoolean;

public class Available_formation_cell extends ListCell<Formation> {
    private AnchorPane layout;
    private Label formation_name;
    private Circle formation_icon;

    private JFXCheckBox select_box;

    public Available_formation_cell(){
    try{
        FXMLLoader load_available_formations_layout=new FXMLLoader(getClass().getResource("/Formation/available_formation_layout.fxml"));
        layout=load_available_formations_layout.load();
        this.formation_name= (Label) load_available_formations_layout.getNamespace().get("formation_name");
        this.formation_icon =(Circle) load_available_formations_layout.getNamespace().get("formation_icon");
        this.select_box=(JFXCheckBox) load_available_formations_layout.getNamespace().get("select_box");
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    @Override
    protected void updateItem(Formation formation, boolean b) {
        super.updateItem(formation, b);
        select_box.selectedProperty().unbind();
        if(formation==null || b){
            setGraphic(null);
            setText(null);
        }
        else{
            formation_name.setText(formation.getFormation_name());
            Image formation_image=formation.getFormation_image();

       
                formation_icon.setFill(new ImagePattern(formation_image));
            
            if(formation.isSelected_available_group()){
                select_box.setSelected(true);
            }
            else{
                select_box.setSelected(false);
            }
                select_box.selectedProperty().bindBidirectional(formation.selected_available_groupProperty());

            setGraphic(layout);
            setText(null);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Sla*_*law 5

问题

您正在双向绑定复选框的选定属性,但正在尝试unbind()取消绑定它。该方法只能用于解除单向绑定。这意味着您实际上永远不会从项目的选定属性中解除复选框的选定属性的绑定。最重要的是,单元格被重用并且同一属性可以同时存在多个双向绑定。最终,每个复选框都会绑定到多个项目,这意味着对一个项目的更新将传播到所有项目。


解决方案

这里至少有两种解决方案。

正确解绑双向绑定

要取消双向绑定,您必须调用unbindBidirectional(Property). 不幸的是,这将使您的代码变得更加复杂,因为您必须维护对旧代码的引用Property才能解除它的绑定。幸运的是,根据单元及其方法的工作方式,您可以通过在调用之前updateItem调用来获取旧项目。getItem() super.updateItem(...)

这是一个例子。

编队.java

import javafx.beans.property.*;
import javafx.scene.image.Image;

public class Formation {

    private final StringProperty name = new SimpleStringProperty(this, "name");
    public final void setName(String name) { this.name.set(name); }
    public final String getName() { return name.get(); }
    public final StringProperty nameProperty() { return name; }

    private final ObjectProperty<Image> image = new SimpleObjectProperty<>(this, "image");
    public final void setImage(Image image) { this.image.set(image); }
    public final Image getImage() { return image.get(); }
    public final ObjectProperty<Image> imageProperty() { return image; }

    private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
    public final void setSelected(boolean selected) { this.selected.set(selected); }
    public final boolean isSelected() { return selected.get(); }
    public final BooleanProperty selectedProperty() { return selected; }
}
Run Code Online (Sandbox Code Playgroud)

FormationListCell.java

import javafx.beans.binding.Bindings;
import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.image.Image;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

public class FormationListCell extends ListCell<Formation> {

    private VBox container;
    private Label label;
    private Circle circle;
    private CheckBox checkBox;

    @Override
    protected void updateItem(Formation item, boolean empty) {
        Formation oldItem = getItem();

        super.updateItem(item, empty);

        if (empty || item == null) {
            setText(null);
            setGraphic(null);
            if (oldItem != null) {
                unbindState(oldItem);
            }
        } else if (oldItem != item) {
            if (container == null) {
                createGraphic();
            } else if (oldItem != null) {
                unbindState(oldItem);
            }
            setGraphic(container);

            label.textProperty().bind(item.nameProperty());
            circle.fillProperty().bind(Bindings.createObjectBinding(() -> {
                Image image = item.getImage();
                return image == null ? null : new ImagePattern(image);
            }, item.imageProperty()));
            checkBox.selectedProperty().bindBidirectional(item.selectedProperty());
        }
    }

    private void unbindState(Formation oldItem) {
        label.textProperty().unbind();
        label.setText(null); // don't keep a strong reference to the string

        circle.fillProperty().unbind();
        circle.setFill(null); // don't keep a strong reference to the image

        // correctly unbind the check box's selected property
        checkBox.selectedProperty().unbindBidirectional(oldItem.selectedProperty());
    }

    private void createGraphic() {
        label = new Label();

        circle = new Circle(25, null);
        circle.setStroke(Color.BLACK);

        checkBox = new CheckBox();

        container = new VBox(10, label, circle, checkBox);
        container.setAlignment(Pos.TOP_CENTER);
    }
}
Run Code Online (Sandbox Code Playgroud)

主程序.java

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Formation> listView = new ListView<>();
        listView.setCellFactory(lv -> new FormationListCell());
        for (int i = 1; i <= 1000; i++) {
            Formation formation = new Formation();
            formation.setName("Formation " + i);
            formation.setSelected(Math.random() < 0.5);
            listView.getItems().add(formation);
        }

        primaryStage.setScene(new Scene(listView, 600, 400));
        primaryStage.show();
    }
}
Run Code Online (Sandbox Code Playgroud)

使用监听器而不是绑定

您可以直接在方法中设置 UI 状态,而不是使用绑定updateItem。然后在复选框的选定属性上设置一个侦听器,以根据需要更新该项目。但是,为了在Formation其他地方更改项目属性时使 UI 与项目保持同步,您需要将列表视图的项目设置为使用所谓的“提取器”创建的ObservableList项目。提取器允许列表观察其元素的属性,最终意味着当任何项目的属性无效时将调用单元格的方法。updateItem

就我个人而言,我更喜欢绑定解决方案,因为这样它在设置列表视图的单元工厂后“就可以工作”,而这种方法要求您记得将属性设置items为自定义列表。

这是一个例子。

编队.java

与之前的解决方案相比没有变化。

FormationListCell.java

import javafx.geometry.Pos;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.ImagePattern;
import javafx.scene.shape.Circle;

public class FormationListCell extends ListCell<Formation> {

    private VBox container;
    private Label label;
    private Circle circle;
    private CheckBox checkBox;

    @Override
    protected void updateItem(Formation item, boolean empty) {
        super.updateItem(item, empty);
        if (empty || item == null) {
            setText(null);
            setGraphic(null);
            if (container != null) {
                // no need to "clear" check box's selected property
                label.setText(null);
                circle.setFill(null);
            }
        } else {
            if (container == null) {
                createGraphic();
            }
            setGraphic(container);

            label.setText(item.getName());
            circle.setFill(item.getImage() == null ? null : new ImagePattern(item.getImage()));
            checkBox.setSelected(item.isSelected());
        }
    }

    private void createGraphic() {
        label = new Label();

        circle = new Circle(25, null);
        circle.setStroke(Color.BLACK);

        checkBox = new CheckBox();
        // have a listener update the model property
        checkBox.selectedProperty().addListener((obs, oldValue, newValue) -> {
            Formation item = getItem();
            if (item != null && newValue != item.isSelected()) {
                item.setSelected(newValue);
            }
        });
        
        container = new VBox(10, label, circle, checkBox);
        container.setAlignment(Pos.TOP_CENTER);
    }
}
Run Code Online (Sandbox Code Playgroud)

主程序.java

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
import javafx.util.Callback;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Formation> listView = new ListView<>();
        listView.setItems(FXCollections.observableArrayList(formationPropertyExtractor()));
        listView.setCellFactory(lv -> new FormationListCell());
        for (int i = 1; i <= 1000; i++) {
            Formation formation = new Formation();
            formation.setName("Formation " + i);
            formation.setSelected(Math.random() < 0.5);
            listView.getItems().add(formation);
        }

        primaryStage.setScene(new Scene(listView, 600, 400));
        primaryStage.show();
    }

    private Callback<Formation, Observable[]> formationPropertyExtractor() {
        return f -> new Observable[] {f.nameProperty(), f.imageProperty(), f.selectedProperty()};
    }
}
Run Code Online (Sandbox Code Playgroud)