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

标签 multithreading image javafx background imageview

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

在后台加载 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;
                                }
                            }
                        });
                    }
                }
            }
        });
    }

最佳答案

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

创建一个有界 BlockingQueue在加载图像时保存图像。队列的大小可能需要一些调整:太小,您将没有任何“缓冲区”(因此您将无法利用加载速度比平均速度快的任何内容),太大,您可能消耗太多内存。 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);
    }
}

关于multithreading - 在后台和线程中加载 JavaFX 图像,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31172090/

相关文章:

java - 在 BorderPane 中设置节点位置时出错

javafx - 为什么 TableCloumn<S,T> 需要 2 个元素?

multithreading - 如何从单独的单元执行线程同步

java - ThreadLocalRandom 上的随机

c# - 锁定公共(public)对象,并跨模块共享

html - 构建 HTML/CSS 页面但无法加载图像

java - 所有事件均不适用于 JavaFX 的 Eclipse

c++ - 在不使用互斥锁的情况下读取在线程(C++)中运行的对象的属性

css - 用 Logo 图像 : best method for SEO and accessibility? 替换 H1 文本

Android : How to convert view to Bitmap, 没有捕获背景屏幕