JavaFX ListView 自定义单元格

JavaFX ListView custom cells

我已尝试为 ListView 创建自定义单元格,但没有出现...

有我的菜单控制器,我在其中使用 ListView 必须包含项目:

FXML(只是一个空白页来测试):

<AnchorPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="50.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.exalow.easymind.ui.factory.ProjectCell" />
public class MenuController extends Controller implements Initializable {
 
    private final ListView<Project> projectListView = new ListView<>();
 
    public MenuController(EasyMind api, Stage stage) {
        super(api, stage);
    }
 
    @Override
    public void initialize(URL location, ResourceBundle resources) {
 
        ObservableList<Project> observableList = FXCollections.observableArrayList();
        observableList.addAll(api.getProjects());
        projectListView.setItems(observableList);
 
        FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/fxml/ProjectCell.fxml"));
 
        try {
            loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
 
        projectListView.setCellFactory(cellFactory -> loader.getController());
    }

还有我的手机控制器

public class ProjectCell extends ListCell<Project> implements Initializable {
 
    private Project project;
 
    @FXML
    private AnchorPane root;
 
    @FXML
    private ImageView imageView;
 
    @FXML
    private Label nameLabel;
 
    @FXML
    private Label descriptionLabel;
 
    @FXML
    private Label createDateLabel;
 
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        setGraphic(root);
    }
 
    @Override
    protected void updateItem(Project item, boolean empty) {
 
        super.updateItem(item, empty);
 
        if (empty || item == null) {
            setText(null);
            setGraphic(null);
        } else {
 
            if (project.hasImage()) {
                imageView.setImage(new Image(project.getImageUrl()));
            } else imageView.setImage(new Image(project.getDefaultImage()));
 
            nameLabel.setText(item.getName());
            descriptionLabel.setText(item.getDescription());
            createDateLabel.setText(item.getCreateDate().toString());
        }
 
        this.project = item;
    }
}

我没有错误,但是当我打开应用程序时我的 ListView 中没有单元格。

可重现的例子:

已创建任何 ProjectCell...

public class ProjectCellController {

    @FXML
    private AnchorPane root;

    @FXML
    private ImageView imageView;

    @FXML
    private Label nameLabel;

    @FXML
    private Label descriptionLabel;

    @FXML
    private Label createDateLabel;

    private Project project;

    public void setProject(Project project) {

        if (project.hasImage()) {
            imageView.setImage(new Image(project.getImageUrl()));
        } else imageView.setImage(new Image(project.getDefaultImage()));

        nameLabel.setText(project.getName());
        descriptionLabel.setText(project.getDescription());
        createDateLabel.setText(project.getCreateDate().toString());

        this.project = project;
    }
}
public class ProjectCell extends ListCell<Project> {

    private AnchorPane root;

    private ProjectCellController controller;

    public ProjectCell() {

        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/fxml/ProjectCell.fxml"));
            root = loader.load();
            controller = loader.getController();
        } catch (IOException exc) {
            throw new RuntimeException(exc);
        }
    }

    @Override
    protected void updateItem(Project project, boolean empty) {
        if (empty || project == null) {
            setGraphic(null);
        } else {
            controller.setProject(project);
            setGraphic(root);
        }
    }
}

和 FXML(一个基本的空白锚定窗格):

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>


<AnchorPane fx:id="root" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="50.0" prefWidth="200.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.exalow.easymind.ui.controllers.ProjectCellController" />

你的代码有很多问题。

  1. 您正在为每个单元分配相同的单元实例(因为您的单元工厂 returns 每次调用时都是相同的实例)。这违反了 UI 元素只能添加到场景图中一次的规则。
  2. 如果在空单元格上调用 updateItem(),则将图形设置为空。在任何情况下,您都不会将图形设置回任何其他内容。因此,如果一个单元格 曾经 用作空单元格(这通常发生在 ListView 首次创建或显示时),那么它将一直没有内容,即使以后变成非空。
  3. 控制器通常不应是 UI 组件。此规则有一些例外情况(例如使用 custom component pattern),但这通常不是一个好主意,因为它会混淆控制器和视图。

所以这里的策略应该是将cell factory设置为新建cell实例的工厂。该单元格可以在构造函数中加载 FXML 文件(因此每个单元格加载 UI 的新实例,但每个单元格只加载一次)。在 updateItem() 方法中,如果单元格为空,则将图形设置为空,否则根据项目配置控制器并将图形设置为从 FXML 加载的 UI。

类似于:

public class ProjectCellController  {
 
 
    @FXML
    private AnchorPane root;
 
    @FXML
    private ImageView imageView;
 
    @FXML
    private Label nameLabel;
 
    @FXML
    private Label descriptionLabel;
 
    @FXML
    private Label createDateLabel;
 
    public void setProject(Project project) {
        if (project.hasImage()) {
            imageView.setImage(new Image(project.getImageUrl()));
        } else imageView.setImage(new Image(project.getDefaultImage()));
 
        nameLabel.setText(project.getName());
        descriptionLabel.setText(project.getDescription());
        createDateLabel.setText(project.getCreateDate().toString());
    }
}

然后

public class ProjectCell extends ListCell<Project> {

    private AnchorPane root ;
    private ProjectCellController controller ;

    public ProjectCell() {

        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/fxml/ProjectCell.fxml"));
            root = loader.load();
            controller = loader.getController();
        } catch (IOException exc) {
            // this is basically fatal, so just bail here:
            throw new RuntimeException(exc);
        }
    }

    @Override
    protected void updateItem(Project project, boolean empty) {
        super.updateItem(project, empty);
        if (empty || project == null) {
            setGraphic(null);
        } else {
            controller.setProject(project);
            setGraphic(root);
        }
    }
}

最后,相应地更改 FXML 文件的 fx:controller 属性,并设置控制器工厂:

projectListView.setCellFactory(listView -> new ProjectCell());

这是一个简化但完整的示例:

Project.java:

public class Project {
    private final String name ;
    public Project(String name) {
        this.name = name ;
    }
    public String getName() {
        return name ;
    }
}

Main.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.geometry.Insets?>

<BorderPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="MainController">
   <center>
      <ListView fx:id="list" />
   </center>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</BorderPane>

MainController.java:

import javafx.fxml.FXML;
import javafx.scene.control.ListView;

public class MainController {

    @FXML
    private ListView<Project> list ;
    
    public void initialize() {
        for (int i = 1 ; i <= 100 ; i++) {
            list.getItems().add(new Project("Project "+i));
        }
        list.setCellFactory(lv -> new ProjectCell());
    }
}

ProjectCell.java:

import java.io.IOException;

import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.ListCell;

public class ProjectCell extends ListCell<Project> {

    private Parent root ;
    private ProjectCellController controller ;
    
    public ProjectCell() {
        try {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("ProjectCell.fxml"));
            root = loader.load();
            controller = loader.getController() ;
        } catch (IOException exc) {
            throw new RuntimeException(exc);
        }
    }
    
    @Override
    protected void updateItem(Project project, boolean empty) {
        super.updateItem(project, empty);
        if (empty || project==null) {
            setGraphic(null);
        } else {
            controller.setProject(project);
            setGraphic(root);
        }
    }
}

ProjectCell.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>

<VBox xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ProjectCellController">
    <children>
        <Label fx:id="nameLabel" />
    </children>
</VBox>

ProjectCellController.java:

import javafx.fxml.FXML;
import javafx.scene.control.Label;

public class ProjectCellController {

    @FXML
    private Label nameLabel ;
    
    public void setProject(Project project) {
        nameLabel.setText(project.getName());
    }
}

和主要应用程序 class:

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;


public class App extends Application {

    @Override
    public void start(Stage stage) throws IOException {
        Scene scene = new Scene(FXMLLoader.load(getClass().getResource("Main.fxml")), 640, 480);
        stage.setScene(scene);
        stage.show();
    }


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

}