5

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.

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;
                                }
                            }
                        });
                    }
                }
            }
        });
    }
user2166698
  • 51
  • 1
  • 3
  • 2
    JavaFX event driven, so you don't need to spawn new threads for this. You also probably want to pause between displaying images (like a slideshow), so you probably want to use a PauseTransition. You don't want to busy wait on a thread either as that will needlessly consume CPU cycles. You also don't want to modify objects in the active scene graph (like the ImageView in your sample) from another thread, as the sky may fall. I don't have time right now to provide a coded solution for you, so unfortunately this is just a comment. – jewelsea Jul 01 '15 at 23:21
  • I do want images to load as fast as physically possible... 60 times a second is the goal but with the size of image I am using this is probably unobtainable because it is taking about 20ms to load the image. So you are saying use something like a Timeline KeyFrame and poll the image.progressProperty in the JavaFX thread every 5ms or so. This is the approach I started with but it seemed like a bad idea. Also I don't understand why I wouldn't want to "modify object in the active scene graph"? Are you saying I should create a new ImageView for each image as they load from disk? – user2166698 Jul 02 '15 at 02:57
  • Is there a good way to have an event triggered when the Image object is actually done loading? If you instantiate an Image object with a big file and immediately place it in the ImageView it appears blank on the screen until it is loaded. Seem like this would be a common request so people could display a "loading" image during loading. BTW pre-loading the images before "showing" the stage is not possible in this application. There are 1000s of images and they are huge. They must be loaded from disk dynamically. – user2166698 Jul 02 '15 at 13:15

1 Answers1

7

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.

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.

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.

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.

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);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322