JavaFX图像在后台和线程中加载 [英] JavaFX Image Loading in Background and Threads

查看:63
本文介绍了JavaFX图像在后台和线程中加载的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我认为这将是一个简单的问题,但是我很难找到答案.我有一个与JavaFX Scene对象关联的ImageView对象,我想从磁盘加载大图像,并使用ImageView依次显示它们.我一直在尝试找到一种很好的方法来反复检查Image对象,并在完成后台加载后将其设置为ImageView,然后开始加载新的Image对象.我提出的代码(如下)有时有效,有时却无效.我敢肯定,我遇到了JavaFX和线程问题.有时会加载第一个图像并停止.变量"processing"是该类中的一个布尔实例变量.

I thought this would be a simple question but I am having trouble finding an answer. I have a single ImageView object associated with a JavaFX Scene object and I want to load large images in from disk and display them in sequence one after another using the ImageView. I have been trying to find a good way to repeatedly check the Image object and when it is done loading in the background set it to the ImageView and then start loading a new Image object. The code I have come up with (below) works sometimes and sometimes it doesn't. I am pretty sure I am running into issues with JavaFX and threads. It loads the first image sometimes and stops. The variable "processing" is a boolean instance variable in the class.

在后台以JavaFX方式加载图像并将其设置为ImageView的正确方法是什么?

What is the proper way to load an image in JavaFX in the background and set it to the ImageView after it is done loading?

public void start(Stage primaryStage) {

       ... 

       ImageView view = new ImageView();
       ((Group)scene.getRoot()).getChildren().add(view);

       ... 

        Thread th = new Thread(new Thread() {
            public void run() {

                while(true) {
                    if (!processing) {

                        processing = true;         
                        String filename = files[count].toURI().toString();
                        Image image = new Image(filename,true);

                        image.progressProperty().addListener(new ChangeListener<Number>() {
                            @Override public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number progress) {
                                if ((Double) progress == 1.0) {
                                    if (! image.isError()) {
                                        view.setImage(image);
                                    }
                                    count++;
                                    if (count == files.length) {
                                        count = 0;
                                    }
                                    processing = false;
                                }
                            }
                        });
                    }
                }
            }
        });
    }

推荐答案

我实际上认为有一种比您尝试使用的方法更好的通用方法来满足您的应用程序的要求,但这是我实现的最佳答案您描述的方法.

I actually think there's probably a better general approach to satisfying whatever your application's requirements are than the approach you are trying to use, but here is my best answer at implementing the approach you describe.

创建有界的BlockingQueue以在加载图像时容纳图像.队列的大小可能需要一些调整:太小,您将没有任何缓冲区"(因此,您将无法利用比平均速度更快的任何缓冲区),太大而您可能消耗太多内存. BlockingQueue允许您从多个线程安全地访问它.

Create a bounded BlockingQueue to hold the images as you load them. The size of the queue may need some tuning: too small and you won't have any "buffer" (so you won't be able to take advantage of any that are faster to load than the average), too large and you might consume too much memory. The BlockingQueue allows you to access it safely from multiple threads.

创建一个线程,该线程简单地同步循环并加载每个图像,即该线程在加载每个图像时阻塞,并将其存储在BlockingQueue中.

Create a thread that simply loops and loads each image synchronously, i.e. that thread blocks while each image loads, and deposits them in the BlockingQueue.

由于您要尝试每FX帧最多显示一次图像(即60fps),因此请使用

Since you want to try to display images up to once per FX frame (i.e. 60fps), use an AnimationTimer. This has a handle method that is invoked on each frame render, on the FX Application Thread, so you can implement it just to poll() the BlockingQueue, and if an image was available, set it in the ImageView.

这是SSCCE.我还指出了如何在固定时间显示每个图像的位置执行此操作,因为我认为这是一个更常见的用例,可能会帮助其他人寻找相似的功能.

Here's an SSCCE. I also indicated how to do this where you display each image for a fixed amount of time, as I think that's a more common use case and might help others looking for similar functionality.

import java.io.File;
import java.net.MalformedURLException;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javafx.animation.AnimationTimer;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.concurrent.Task;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;


public class ScreenSaver extends Application {

    @Override
    public void start(Stage primaryStage) {

        BorderPane root = new BorderPane();

        Button startButton = new Button("Choose image directory...");
        startButton.setOnAction(e -> {
            DirectoryChooser chooser= new DirectoryChooser();
            File dir = chooser.showDialog(primaryStage);
            if (dir != null) {
                File[] files = Stream.of(dir.listFiles()).filter(file -> {
                    String fName = file.getAbsolutePath().toLowerCase();
                    return fName.endsWith(".jpeg") | fName.endsWith(".jpg") | fName.endsWith(".png");
                }).collect(Collectors.toList()).toArray(new File[0]);
                root.setCenter(createScreenSaver(files));
            }
        });

        root.setCenter(new StackPane(startButton));

        primaryStage.setScene(new Scene(root, 800, 800));
        primaryStage.show();
    }

    private Parent createScreenSaver(File[] files) {
        ImageView imageView = new ImageView();
        Pane pane = new Pane(imageView);
        imageView.fitWidthProperty().bind(pane.widthProperty());
        imageView.fitHeightProperty().bind(pane.heightProperty());
        imageView.setPreserveRatio(true);

        Executor exec = Executors.newCachedThreadPool(runnable -> {
            Thread t = new Thread(runnable);
            t.setDaemon(true);
            return t ;
        });

        final int imageBufferSize = 5 ;
        BlockingQueue<Image> imageQueue = new ArrayBlockingQueue<Image>(imageBufferSize);

        exec.execute(() -> {
            int index = 0 ;
            try {
                while (true) {
                    Image image = new Image(files[index].toURI().toURL().toExternalForm(), false);
                    imageQueue.put(image);
                    index = (index + 1) % files.length ;
                }
            } catch (MalformedURLException e) {
                throw new RuntimeException(e);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // This will show a new image every single rendering frame, if one is available: 
        AnimationTimer timer = new AnimationTimer() {

            @Override
            public void handle(long now) {
                Image image = imageQueue.poll();
                if (image != null) {
                    imageView.setImage(image);
                }
            }
        };
        timer.start();

        // This wait for an image to become available, then show it for a fixed amount of time,
        // before attempting to load the next one:

//        Duration displayTime = Duration.seconds(1);
//        PauseTransition pause = new PauseTransition(displayTime);
//        pause.setOnFinished(e -> exec.execute(createImageDisplayTask(pause, imageQueue, imageView)));
//        exec.execute(createImageDisplayTask(pause, imageQueue, imageView));

        return pane ;
    }

    private Task<Image> createImageDisplayTask(PauseTransition pause, BlockingQueue<Image> imageQueue, ImageView imageView) {
        Task<Image> imageDisplayTask = new Task<Image>() {
            @Override
            public Image call() throws InterruptedException {
                return imageQueue.take();
            }
        };
        imageDisplayTask.setOnSucceeded(e -> {
            imageView.setImage(imageDisplayTask.getValue());
            pause.playFromStart();
        });
        return imageDisplayTask ;
    }

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

这篇关于JavaFX图像在后台和线程中加载的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆