Search code examples
javajavafx

WatchListener add images to scrollpane


I have a program that watches a folder for images added to it. Then takes those images and add them to a scroll pane for the end user to view. The end users are also able to add images manually as well - this part I have figured out.

I'm able to successfully have the program watch the folder and print to console if a file has been created in the respective directory. However I cannot figure out how to add the images added to the watched folder added to the listview.

ImagesController.java

public class ImagesControllerimplements Initializable {
    public Button addNewButton;
    public Pane pane;
    public ImageView imageview1;
    public ListView photoListView;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        new Thread(this::onWatchListener).start();
    }

    public void onAddNew(ActionEvent event) {
        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Select file to add to Patient's account");

        fileChooser.getExtensionFilters().add(
                new FileChooser.ExtensionFilter("Image Files", "*.png", "*jpg")
        );

        fileChooser.setInitialDirectory(new File("C:\\dev"));
        File selectedFile = fileChooser.showOpenDialog(new Stage());

        if (selectedFile != null) {
            Image newImage = new Image(selectedFile.toURI().toString());
            imageview1.setImage(newImage);
            imageview1.setFitWidth(250);
            imageview1.setFitHeight(250);
        }
    }

    public void onWatchListener() {
        try {
            Path directoryPath = Paths.get("C:\\Users\\images");

            WatchService watchService = FileSystems.getDefault().newWatchService();

            directoryPath.register(watchService,
                    StandardWatchEventKinds.ENTRY_CREATE);

            System.out.println("Watching directory: " + directoryPath);

            while (true) {
                ListView<String> events = new ListView<>();
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                        System.out.println("File created: " + event.context());
                        events.getItems().add(event.toString());
                    }
                }
                key.reset();
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

}

I've tried a few different things -

I tried to create a listener from the listview to add in the events from the watchlister

 public void onWatchListener() {
        try {
            Path directoryPath = Paths.get("C:\\Users\\images");

            WatchService watchService = FileSystems.getDefault().newWatchService();

            directoryPath.register(watchService,
                    StandardWatchEventKinds.ENTRY_CREATE);

            System.out.println("Watching directory: " + directoryPath);

            while (true) {
                WatchKey key = watchService.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                        System.out.println("File created: " + event.context());
                        photoListView.itemsProperty().addListener(new ChangeListener() {
                            @Override
                            public void changed(ObservableValue observable, Object oldValue, Object newValue) {
                                if(newValue != null){
                                    photoListView.getItems().add(event);
                                }
                            }
                        });
                    }
                }
                key.reset();
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }

This did nothing and I'm unclear on why this approach didn't work.

Should the watchlistener be setup as service and called into the initialize method? Then attach the listview to the watchlistener service and add the WatchKey event to the listview?


Solution

  • It's not really clear what the code you posted is supposed to do. I am guessing that ImagesController is the controller class for an FXML file. And presumably (though it is not stated) the public fields are defined in the FXML file.

    In the first version of onWatchListener() (not sure why the method is called that) you appear to create a new ListView every time a new file is detected (or, at least, every time you get a new WatchKey`, which could theoretically correspond to multiple files being created); you then add items to this new list view, which you then discard without putting it in the scene graph anywhere. It's not really clear why you are creating a new list view in the first place when you already have one defined by the FXML file.

    In the second version of onWatchListener(), every time a new file is added, you add a new listener to the list view's items property. These listeners (all of them) will be invoked any time the items property changes, and when that happens new items will be added to the list view.

    Of course, since you never change the items property, none of these listeners are ever invoked, so nothing happens.

    All you need to do is add a new item to the list view every time a new file is detected. Since this code is running on a background thread, you need to ensure that the items are added to the list view on the FX Application Thread, which you can do with a simple call to Platform.runLater().

    Here is a basic example of a controller that works. Note this application creates a folder called "Images" in the user's home directory. If you copy images to that folder, they will appear in the list view and the image view.

    package org.jamesd.examples.imagewatch;
    
    import javafx.application.Platform;
    import javafx.fxml.FXML;
    import javafx.scene.control.ListCell;
    import javafx.scene.control.ListView;
    import javafx.scene.image.Image;
    import javafx.scene.image.ImageView;
    
    import java.io.IOException;
    import java.nio.file.*;
    
    public class ImageWatchController {
    
        private static final Path IMAGES_FOLDER = Paths.get(
             System.getProperty("user.home"), "Images"
        );
        @FXML
        private ListView<Image> images;
    
        @FXML
        private ImageView imageView ;
    
        @FXML
        private void initialize() throws IOException {
            if (! Files.exists(IMAGES_FOLDER)) {
                Files.createDirectory(IMAGES_FOLDER);
            }
            Thread watchFilesThread = new Thread(this::watchFiles);
            watchFilesThread.setDaemon(true);
            watchFilesThread.start();
    
            images.getSelectionModel().selectedItemProperty().subscribe(imageView::setImage);
            images.setCellFactory(_ -> new ListCell<>(){
                @Override
                protected void updateItem(Image image, boolean empty) {
                    super.updateItem(image, empty);
                    if (empty || image == null) {
                        setText(null);
                    } else {
                        // TODO: make this prettier
                        setText(image.getUrl());
                    }
                }
            });
        }
    
        private void watchFiles() {
            try {
                WatchService watchService = FileSystems.getDefault().newWatchService();
                IMAGES_FOLDER.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);
                boolean done = false;
                while (! done) {
                    WatchKey key = watchService.take();
                    for (WatchEvent<?> event : key.pollEvents()) {
                        if (event.kind().type() == Path.class) {
                            Path path = ((WatchEvent<Path>)event).context();
                            Platform.runLater(() -> addImage(IMAGES_FOLDER.resolve(path)));
                        }
                        done = ! key.reset();
                    }
                }
            } catch (Exception exc) {
                exc.printStackTrace();
            }
    
        }
    
        private void addImage(Path imagePath) {
            Image image = new Image(imagePath.toUri().toString());
            image.exceptionProperty().subscribe(e -> {
                if (e != null) {
                    e.printStackTrace();
                }
            });
            images.getItems().add(image);
            images.getSelectionModel().select(image);
        }
    }
    

    and for completeness a simple FXML file:

    ImageWatch.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.ListView?>
    <?import javafx.scene.image.ImageView?>
    <?import javafx.scene.layout.BorderPane?>
    <BorderPane xmlns="http://javafx.com/javafx"
                xmlns:fx="http://javafx.com/fxml"
                fx:controller="org.jamesd.examples.imagewatch.ImageWatchController">
    
        <left>
            <ListView fx:id="images"/>
        </left>
        <center>
            <ImageView fx:id="imageView"/>
        </center>
    </BorderPane>
    

    and the application:

    package org.jamesd.examples.imagewatch;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class ImageWatchApp extends Application {
        @Override
        public void start(Stage stage) throws Exception {
            FXMLLoader loader = new FXMLLoader(getClass().getResource("ImageWatch.fxml"));
            Parent root = loader.load();
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    }