【问题标题】:JavaFx Combobox lazy loading imagesJavaFx Combobox 延迟加载图像
【发布时间】:2014-10-31 21:30:33
【问题描述】:

我是第一次使用 JavaFX 组合框...我在组合框中有超过 2000 个图标(可以通过我从 StackOverflow 中找到的 AutoCompleteComboBoxListener 过滤它们)。

我打算使用 ExecutorService 从 zip 文件中获取图像。

现在的问题是,我如何才能找出 Combobox 中当前可见的项目?

我正在为 ComboBox 设置一个自定义 ListCellFactory,并且我有一个自定义 ListCell 类,它也显示图标。我可以从 ListCell 对象中检查项目是否“正在显示”吗?

谢谢。

【问题讨论】:

  • 为什么需要检查?
  • 就像我说的那样,ComboBox 的弹出窗口中可能同时有 2000 个图标,我的计划是加载并显示在 ComboBox 弹出窗口的视口中可见的图标 - 当用户滚动弹出窗口的滚动窗格。
  • 细胞机制会处理这个问题(或多或少)。添加了答案。

标签: java combobox javafx


【解决方案1】:

即使您的列表有 2000 个项目,javafx 也只会为可见单元格创建 listcell 对象(加上一个或两个以上半可见单元格),所以实际上没有太多 TODO 可以让您延迟加载图像 - 只需在 updateItem 时加载它们被调用 - 并且可能将已加载的图像缓存在 lifo 缓存中,以便并非所有图像都留在内存中

【讨论】:

  • 如果您在 FX 应用程序线程上按需从 zip 文件中提取图像,滚动可能会非常难看,不是吗?
【解决方案2】:

首先请注意,如果您从单个文件而不是从 zip 文件加载图像,则有一种机制可以完全避免直接使用任何类型的线程:

ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> new ListCell<MyDataType>() {

    private ImageView imageView = new ImageView();

    @Override
    public void updateItem(MyDataType item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            String imageURL = item.getImageURL();
            Image image = new Image(imageURL, true); // true means load in background
            imageView.setImage(image);
            setGraphic(imageView);
        }
    }
});

不幸的是,如果您从 zip 文件加载,我认为您不能使用它,因此您需要创建自己的后台任务。如果单元格中的项目在加载过程中发生变化(如果用户滚动很多,这很可能),您需要小心一点,以确保您不使用在后台加载的图像。

(更新说明:我将其更改为监听单元格的itemProperty() 的变化,而不是使用updateItem(...) 方法。updateItem(...) 方法可以比实际显示的项目更频繁地调用单元格更改,因此这种方法避免了不必要的图像“重新加载”。)

类似:

ExecutorService exec = ... ;
ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> {

    ListCell<MyDataType> cell = new ListCell<MyDataType>() ;

    ImageView imageView = new ImageView();
    ObjectProperty<Task<Image>> loadingTask = new SimpleObjectProperty<>();

    cell.emptyProperty().addListener((obs, wasEmpty, isNotEmpty) -> {
        if (isNowEmpty) {
            cell.setGraphic(null);
            cell.setText(null);
        } else {
            cell.setGraphic(imageView);
        }
    });

    cell.itemProperty().addListener((obs, oldItem, newItem) ->  {
        if (loadingTask.get() != null && 
            loadingTask.get().getState() != Worker.State.SUCCEEDED &&
            loadingTask.get().getState() != Worker.State.FAILED) {

            loadingTask.get().cancel();
        }
        loadingTask.set(null) ;
        if (newItem != null) {
            Task<Image> task = new Task<Image>() {
                @Override
                public Image call() throws Exception {
                    Image image = ... ; // retrieve image for item
                    return image ;
                }
            };
            loadingTask.set(task);
            task.setOnSucceeded(event -> imageView.setImage(task.getValue()));
            exec.submit(task);

            cell.setText(...); // some text from item...
        }
    }); 

    return cell ;
});

关于性能的一些想法:

首先,ComboBox 的“虚拟化”机制意味着只会创建少量这些单元格,因此您不必担心会立即启动数千个线程加载图像,或者事实上,你将永远有成千上万的图像在记忆中。

当用户滚动列表时,itemProperty(...) 可能会频繁更改,因为单元格会重复用于新项目。确保您不使用已启动但在项目再次更改之前未完成的线程中的图像很重要;这是在项目更改侦听器开始时取消现有任务的目的。取消任务将阻止调用onSucceeded 处理程序。但是,这些线程仍然在运行,所以如果可能,call() 方法的实现应该检查isCancelled() 标志,如果它返回 true,则尽快中止。这可能很难实现:我会先试验一下,看看它是如何与一个简单的实现一起工作的。

【讨论】:

  • 非常感谢您的全面回答。我不知道 Image 可以开箱即用地进行背景加载。而且,Image 的 javadoc 声明它也从类路径中搜索,并且我的 zip 文件与我的应用程序捆绑在一起,所以我将它添加到类路径中没有问题......我一切正常。
【解决方案3】:

当前可见项表示组合框中当前选定的项。您可以使用

获取所选项目
comboboxname.getSelectionModel().getSelectedItem();

【讨论】:

  • 嗯,我的意思是组合框弹出窗口中当前可见的项目。
  • 什么弹出窗口?你的意思是自动完成弹出窗口?
  • 对不起,如果我不清楚。我指的是当您单击组合框上的向下箭头按钮时弹出的弹出窗口,可见项目是指当前显示在弹出窗口的 ScrollPane 中的项目。
  • 滚动窗格大小取决于。你确定你试图做的是最好的方法吗?
  • 不,我不是。 :) 我对想法持开放态度,我以前从未真正实施过这样的事情。
猜你喜欢
  • 2017-04-17
  • 1970-01-01
  • 2013-10-24
  • 2021-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-08-12
相关资源
最近更新 更多