Search code examples
javaswingjlistrepaint

JList repaint single element


I've got a JList whose elements consist of image files for which I'm creating thumbnails (in a background Thread). When these thumbnails become available, I'd like to force a repaint of just that item. However, I find that when I use the listModel's fireDataChanged method (see below), all the visible items in the list are repainted (using my custom ListCellRenderer).

public void updateElement(int index) {
    frame.listModel.fireContentsChanged(frame.listModel, index, index);
}

Is there any way to cause ONLY the indexed item to be repainted?


Solution

  • I find that when I use the listModel's fireDataChanged method (see below), all the visible items in the list are repainted

    You should NOT invoke that method manually. The fireXXX(...) methods should only be invoked by the model itself.

    You should be updating the model by using the:

    model.set(...);
    

    The set(...) method will then invoke the appropriate method to notify the JList to repaint the cell.

    Here is my attempt at a simple Thumbnail app. It attempts to add performance improvements by:

    1. loading the model with a default Icon so the list doesn't continually need to resize itself
    2. Use a ExecutorService to take advantage of multiple processors
    3. Using an ImageReader to read the file. The sub sampling property allows you to use fewer pixels when scaling the image.

    Just change the class to point to a directory containing some .jpg files and give it a go:

    ThumbnailApp:

    import java.io.*;
    import java.util.concurrent.*;
    import java.awt.*;
    //import java.awt.datatransfer.*;
    import java.awt.event.*;
    import javax.swing.*;
    
    class ThumbnailApp
    {
        private DefaultListModel<Thumbnail> model = new DefaultListModel<Thumbnail>();
        private JList<Thumbnail> list = new JList<Thumbnail>(model);
    
        public ThumbnailApp()
        {
        }
    
        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 );
            cp.add(new JScrollPane( list ), BorderLayout.CENTER);
    
            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");});
    
                int processors = Runtime.getRuntime().availableProcessors();
                ExecutorService service = Executors.newFixedThreadPool( processors - 2 );
    
                long start = System.currentTimeMillis();
    
                for (File file: files)
                {
                    Thumbnail thumbnail = new Thumbnail(file, null);
                    model.addElement( thumbnail );
    //              new ThumbnailWorker(file, model, model.size() - 1).execute();
                    service.submit( new ThumbnailWorker(file, model, model.size() - 1) );
                }
    
                long duration = System.currentTimeMillis() - start;
                System.out.println(duration);
    
                service.shutdown();
            }
            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());
        }
    }
    

    ThumbnailWorker:

    import java.awt.*;
    import java.awt.image.*;
    import java.io.*;
    import java.util.Iterator;
    //import java.util.concurrent.*;
    import javax.imageio.*;
    import javax.imageio.stream.*;
    import javax.swing.*;
    
    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.getFileName() );
    
            return this;
        }
    }
    

    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 String getFileName()
        {
            return file.getName();
        }
    }
    

    I tested on a directory with 302 images. Using the ExecutorService got the load time down from 2:31 to 0:35.