0

I want to load multiple images into a JPanel, and I prefer to load them on demand rather than all at once, especially since I'm using JScrollPane. Most of the images I load are from direct URLs, which I store in an array of Strings. I load them one by one in a for loop using my ImageLoader method

private void loadOnlineImages(String[] images, int maxWidth, int maxHeight) {
        imagesPanel.removeAll();
        imagesPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
        footerPanel.setTotalItems(images.length);

        for (String image : images) {
            ImageLoader onlineImageLoader =
                    new ImageLoader(imagesPanel, footerPanel, image, maxWidth, maxHeight);
            onlineImageLoader.loadImage();
        }

        imagesPanel.revalidate();
        imagesPanel.repaint();
    }

I tried to use imagesPanel.isDisplayable and expected it to load the images only when they're visible in the JScrollPane, but it didn't work, and all the images still load simultaneously, which freezes the application. Most of the images I load are 10-50 KBs, so when I load 20 images, it doesn't freeze, but when I load 100, it freezes.

Here is the ImageLoader class used to load the images.

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.util.Iterator;

public class ImageLoader {
    ProgressListener progressListener;
    JPanel footerPanel;
    JPanel imagesPanel;
    String imageUrl;
    int maxWidth;
    int maxHeight;

    public ImageLoader(JPanel imagesPanel, JPanel footerPanel, String imageUrl, int maxWidth, int maxHeight) {
        this.imagesPanel = imagesPanel;
        this.imageUrl = imageUrl;
        this.maxWidth = maxWidth;
        this.maxHeight = maxHeight;
        this.footerPanel = footerPanel;
        progressListener = new ProgressListener(footerPanel);
    }

    // Load the image only when it becomes visible
    public void loadImage() {
        new Thread(() -> {
            try {
                URI uri = URI.create(imageUrl);
                ImageInputStream imageInputStream = ImageIO.createImageInputStream(uri.toURL().openStream());
                Iterator<ImageReader> iterator = ImageIO.getImageReaders(imageInputStream);
                if (iterator.hasNext()) {
                    ImageReader reader = iterator.next();
                    reader.setInput(imageInputStream);
                    reader.addIIOReadProgressListener(progressListener);
                    BufferedImage image = reader.read(reader.getMinIndex());
                    final ImageIcon icon = new ImageIcon(image);

                    // Check if the image is still required to be shown
                    if (imagesPanel.isDisplayable()) {
                        SwingUtilities.invokeLater(() -> {
                            JLabel imageLabel = new JLabel(IconScaler.createScaledIcon(icon, maxWidth, maxHeight));
                            imagesPanel.add(imageLabel);
                            imagesPanel.revalidate();
                            imagesPanel.repaint();
                        });
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

Thank you so much in advance for your assistance!

John
  • 5
  • 2
  • Not entirely sure this will help, but you might consider having a look at something like [this](https://stackoverflow.com/questions/8485893/get-shown-component-in-jscrollpane) – MadProgrammer Jul 26 '23 at 11:04
  • The images load simultaneously because you have a loop that loads them all at the same time. However, I'm not sure why the application freezes since you appear to be using a Thread to load the images and then use invokeLater() to add each label to the panel. So that should NOT block the EDT. Maybe the issue is that you are trying to create 100 separate Threads to load the image? Note that with your current logic you can't control the order in which the images are displayed since you can't control the order in which each Thread finishes executing. – camickr Jul 26 '23 at 14:20
  • Check out: https://stackoverflow.com/a/67348202/131872 for my attempt to load thumbnail images into a JList. It uses a SwingWorker to load/publish the image. It takes advantage of the ExecutorService which will limit the number of SwingWorkers running to the number of CPU cores available on your system. It will still take a fixed time to load the images but the GUI is not frozen during loading. You can easily test this by resizing the frame and you will see the rows/columns of the JList change during loading. For a quick test you just provide the directory where the image are located. – camickr Jul 26 '23 at 14:24
  • 1
    @camickr [stackoverflow.com/a/67348202/131872](https://stackoverflow.com/a/67348202/131872), this definitely solves my problem regarding why my application freezes. As for the lazy loading, I think I'll change the way I load my images. I didn't realize I was loading all of them at the same time. I'll try to apply the approach shown in the [link](https://stackoverflow.com/questions/8485893/get-shown-component-in-jscrollpane) given by @ MadProgrammer. Thank you very much for responding to my problem. – John Jul 27 '23 at 00:28

1 Answers1

1

Using some of the suggestions in the link provided by MadProgrammer, I modified the code found in my link from above to do lazy loading.

The basics changes are to:

  1. initially load the ListModel with a Thumbnail object that contains the File but a null Icon
  2. create a Set to contain all the Files to be loaded
  3. add a ChangeListener to the viewport of the scroll pane
  4. when the ChangeListener is invoked I get all the indexes of the images currently visible in the viewport. I then check the File of each image to see if it needs to be loaded. If so, I invoke the ThumbnailWorker to load the image (and remove it from the set of files to be loaded).

Here are the new classes:

ThumbnailApp

import java.io.*;
import java.util.concurrent.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ThumbnailApp
{
    private DefaultListModel<Thumbnail> model = new DefaultListModel<Thumbnail>();
    private JList<Thumbnail> list = new JList<Thumbnail>(model);
    private Set<File> filesToBeLoaded = new HashSet<>();
    private ExecutorService service;

    public ThumbnailApp()
    {
        int processors = Runtime.getRuntime().availableProcessors();
        service = Executors.newFixedThreadPool( processors - 2 );
    }

    public JPanel createContentPane()
    {
        JPanel cp = new JPanel( new BorderLayout() );

        list.setCellRenderer( new ThumbnailRenderer<Thumbnail>() );
        list.setLayoutOrientation(JList.HORIZONTAL_WRAP);
        list.setVisibleRowCount(-1);
        Icon empty = new EmptyIcon(160, 160);
        Thumbnail prototype = new Thumbnail(new File("PortugalSpain-000.JPG"), empty);
        list.setPrototypeCellValue( prototype );

        JScrollPane scrollPane = new JScrollPane( list );
        cp.add(scrollPane, BorderLayout.CENTER);

        scrollPane.getViewport().addChangeListener((e) ->
        {
            int first = list.getFirstVisibleIndex();
            int last = list.getLastVisibleIndex();
            System.out.println(first + " : " + last);

            if (first == -1) return;

            for (int i = first; i <= last; i++)
            {
                Thumbnail thumbnail = model.elementAt(i);
                File file = thumbnail.getFile();

                if (filesToBeLoaded.contains(file))
                {
                    filesToBeLoaded.remove(file);
                    service.submit( new ThumbnailWorker(thumbnail.getFile(), model, i) );
                }
            }

            if (filesToBeLoaded.isEmpty())
                service.shutdown();
        });

        return cp;
    }

    public void loadImages(File directory)
    {
        new Thread( () -> createThumbnails(directory) ).start();
    }

    private void createThumbnails(File directory)
    {
        try
        {
            File[] files = directory.listFiles((d, f) -> {return f.endsWith(".JPG");});

            for (File file: files)
            {
                filesToBeLoaded.add( file );
                Thumbnail thumbnail = new Thumbnail(file, null);
                model.addElement( thumbnail );
            }
        }
        catch(Exception e) { e.printStackTrace(); }
    }

    private static void createAndShowGUI()
    {
        ThumbnailApp app = new ThumbnailApp();

        JFrame frame = new JFrame("ListDrop");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane( app.createContentPane() );
        frame.setSize(1600, 900);
        frame.setVisible(true);

//      File directory = new File("C:/Users/netro/Pictures/TravelSun/2019_01_Cuba");
        File directory = new File("C:/Users/netro/Pictures/TravelAdventures/2018_PortugalSpain");
        app.loadImages( directory );
    }
    public static void main(String[] args)
    {
        javax.swing.SwingUtilities.invokeLater(() -> createAndShowGUI());
    }
}

Thumbnail

import java.io.File;
import javax.swing.Icon;

public class Thumbnail
{
    private File file;
    private Icon icon;

    public Thumbnail(File file, Icon icon)
    {
        this.file = file;
        this.icon = icon;
    }

    public Icon getIcon()
    {
        return icon;
    }

    public void setIcon(Icon icon)
    {
        this.icon = icon;
    }

    public File getFile()
    {
//      return file.getName();
        return file;
    }
}

ThumbnailWorker

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Iterator;
import javax.imageio.*;
import javax.imageio.stream.*;
import javax.swing.*;

public class ThumbnailWorker extends SwingWorker<Image, Void>
{
    private File file;
    private DefaultListModel<Thumbnail> model;
    private int index;

    public ThumbnailWorker(File file, DefaultListModel<Thumbnail> model, int index)
    {
        this.file = file;
        this.model = model;
        this.index = index;
    }

    @Override
    protected Image doInBackground() throws IOException
    {
//      Image image = ImageIO.read( file );

        Iterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("jpg");
        ImageReader reader = readers.next();
        ImageReadParam irp = reader.getDefaultReadParam();
//      irp.setSourceSubsampling(10, 10, 0, 0);
        irp.setSourceSubsampling(5, 5, 0, 0);
        ImageInputStream stream = new FileImageInputStream( file );
        reader.setInput(stream);
        Image image = reader.read(0, irp);

        int width = 160;
        int height = 90;

        if (image.getHeight(null) > image.getWidth(null))
        {
            width = 90;
            height = 160;
        }

        BufferedImage scaled = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g2d = scaled.createGraphics();
        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

        g2d.drawImage(image, 0, 0, width, height, null);
        g2d.dispose();
        image = null;

        return scaled;
   }

   @Override
   protected void done()
   {
       try
       {
           ImageIcon icon = new ImageIcon( get() );
           Thumbnail thumbnail = model.get( index );
           thumbnail.setIcon( icon );
           model.set(index, thumbnail);
//         System.out.println("finished: " + file);
       }
       catch (Exception e)
       {
           e.printStackTrace();
       }
   }
}

ThumbnailRenderer

import java.awt.Component;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

public class ThumbnailRenderer<E> extends JLabel implements ListCellRenderer<E>
{
    public ThumbnailRenderer()
    {
        setOpaque(true);
        setHorizontalAlignment(CENTER);
        setVerticalAlignment(CENTER);
        setHorizontalTextPosition( JLabel.CENTER );
        setVerticalTextPosition( JLabel.BOTTOM );
        setBorder( new EmptyBorder(4, 4, 4, 4) );
    }

    /*
     *  Display the Thumbnail Icon and file name.
     */
    public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus)
    {
        if (isSelected)
        {
            setBackground(list.getSelectionBackground());
            setForeground(list.getSelectionForeground());
        }
         else
        {
            setBackground(list.getBackground());
            setForeground(list.getForeground());
        }

        //Set the icon and filename

        Thumbnail thumbnail = (Thumbnail)value;
        setIcon( thumbnail.getIcon() );
        setText( thumbnail.getFile().getName() );

//      System.out.println(thumbnail.getFileName());

        return this;
    }
}

EmptyIcon

import java.awt.*;
import javax.swing.*;

public class EmptyIcon implements Icon
{
    private int width;
    private int height;

    public EmptyIcon(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

    public int getIconWidth()
    {
        return width;
    }

    public int getIconHeight()
    {
        return height;
    }

    public void paintIcon(Component c, Graphics g, int x, int y)
    {
    }
}

You might also want to consider removing the ChangeListener from the viewport once the filesToBeLoaded Set is empty.

camickr
  • 321,443
  • 19
  • 166
  • 288