在后台和线程中加载JavaFX图像

时间:2015-07-01 21:33:47

标签: multithreading image javafx background imageview

我认为这是一个简单的问题,但我找不到答案。我有一个与JavaFX Scene对象关联的ImageView对象,我想从磁盘加载大图像,并使用ImageView依次显示它们。我一直在试图找到一种重复检查Image对象的好方法,当它在后台加载完成后,将它设置为ImageView,然后开始加载一个新的Image对象。我提出的代码(下面)有时会工作,有时则不会。我很确定我遇到了JavaFX和线程的问题。它有时加载第一张图像并停止。变量“processing”是类中的布尔实例变量。

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

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;
                                }
                            }
                        });
                    }
                }
            }
        });
    }

1 个答案:

答案 0 :(得分:5)

我实际上认为,对于满足您的应用程序要求而言,可能是一种更好的通用方法,而不是您尝试使用的方法,但这是我实施您描述的方法的最佳答案。 / p>

创建有界BlockingQueue以在加载图片时保留图像。队列的大小可能需要一些调整:太小而你不会有任何&#34;缓冲区&#34; (所以你不能利用加载速度比平均速度快的任何东西),太大而你可能会消耗太多内存。 BlockingQueue允许您从多个线程安全地访问它。

创建一个简单地循环并加载每个图像同步的线程,即在每个图像加载时线程阻塞,并将它们存放在BlockingQueue中。

由于您想尝试每个FX帧最多显示一次图像(即60fps),请使用AnimationTimer。这有一个handle方法,在FX应用程序线程上的每个帧渲染上调用,因此您只需poll() BlockingQueue即可实现它,如果图像可用,则设置它在ImageView

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

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);
    }
}