-1

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?

Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
mstoreysmith
  • 81
  • 1
  • 5
  • Is the size of the element changing once the thumbnail is loaded - forcing a re-layout? – Mr R Apr 30 '21 at 23:10
  • Honestly, you shouldn't be calling `fireContentsChange` in this way - it really should be a `protected` method (IMHO). You might consider trying to remove and reinsert the element? – MadProgrammer Apr 30 '21 at 23:16
  • Fill with blank Icons, sized correctly, and then insert the Icon with the image in the correct model position once the image has been created. A SwingWorker publish/process pair could work nicely here, no? – Hovercraft Full Of Eels Apr 30 '21 at 23:18
  • The element size is hard coded, so no it doesn't change. – mstoreysmith May 01 '21 at 03:12
  • *"I'd like to force a repaint of **just** that item."* Smells like premature optimization to me. **General Tips:** 1) For better help sooner, [edit] to add a [MCVE] or [Short, Self Contained, Correct Example](http://www.sscce.org/). 2) One way to get image(s) for an example is to hot link to images seen in [this Q&A](http://stackoverflow.com/q/19209650/418556). E.G. The code in [this answer](https://stackoverflow.com/a/10862262/418556) hot links to an image embedded in [this question](https://stackoverflow.com/q/10861852/418556). – Andrew Thompson May 01 '21 at 07:22
  • *"The element size is hard coded, so no it doesn't change."* Tip: Add @MrR (or whoever, the `@` is important) to *notify* the person of a new comment. – Andrew Thompson May 01 '21 at 07:23
  • Further investigation reveals this is not just an issue with image-loading. It happens with item selection, too. When I select an element in the list, *all* are getting redrawn. Is there a method I can override or some other step I can take to have the list redraw just the updated cells, not all of them? – mstoreysmith May 01 '21 at 13:23
  • @mstoreysmith *Further experimentation demonstrates the JList is also updating all visible elements when I select a new element.* - I find that when you select an item in a different row all the items in that row are repainted, not all visible elements. – camickr May 01 '21 at 16:08
  • Right you are, @camickr. It's all items in the row that are repainted. Seems excessive when you're changing just one - or two (selected item & deselected item). Anybody know how to override this behavior? – mstoreysmith May 02 '21 at 13:02
  • *Anybody know how to override this behavior?* - my point was it is not excessive because it is NOT all visible elements. This logic is done behind the scenes and imbedded into the UI and will not be easy to change. It is not a performance issue so there is nothing to worry about. – camickr May 02 '21 at 14:40

2 Answers2

2

Without some kind of runnable example which demonstrates your issue, it's impossible to make any concrete recommendations.

The following simple example makes use of a SwingWorker to change the value of the elements within the ListModel. To make it look more realistic, I've shuffled the List of indices and applied a short delay between each.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private DefaultListModel<String> model = new DefaultListModel<>();

        public TestPane() {
            setLayout(new BorderLayout());
            add(new JScrollPane(new JList(model)));

            JButton load = new JButton("Load");
            add(load, BorderLayout.SOUTH);

            load.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent arg0) {
                    load.setEnabled(false);
                    model.removeAllElements();

                    for (int index = 0; index < 100; index++) {
                        model.addElement("[" + index + "] Loading...");
                    }

                    LoadWorker worker = new LoadWorker(model);
                    worker.addPropertyChangeListener(new PropertyChangeListener() {
                        @Override
                        public void propertyChange(PropertyChangeEvent evt) {
                            System.out.println(evt.getPropertyName() + " == " + evt.getNewValue());
                            if ("state".equals(evt.getPropertyName())) {
                                Object value = evt.getNewValue();
                                if (value instanceof SwingWorker.StateValue) {
                                    SwingWorker.StateValue stateValue = (SwingWorker.StateValue) value;
                                    if (stateValue == SwingWorker.StateValue.DONE) {
                                        load.setEnabled(true);
                                    }
                                }
                            }
                        }
                    });
                    worker.execute();
                }
            });
        }

    }

    public class LoadResult {

        private int index;
        private String value;

        public LoadResult(int index, String value) {
            this.index = index;
            this.value = value;
        }

        public int getIndex() {
            return index;
        }

        public String getValue() {
            return value;
        }

    }

    public class LoadWorker extends SwingWorker<Void, LoadResult> {

        private DefaultListModel model;

        public LoadWorker(DefaultListModel model) {
            this.model = model;
        }

        public DefaultListModel getModel() {
            return model;
        }

        @Override
        protected void process(List<LoadResult> chunks) {
            for (LoadResult loadResult : chunks) {
                model.set(loadResult.index, loadResult.value);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            int count = model.getSize();
            List<Integer> indicies = new ArrayList<>(count);
            for (int index = 0; index < count; index++) {
                indicies.add(index);
            }
            Collections.shuffle(indicies);
            for (int index : indicies) {
                Thread.sleep(15);
                publish(new LoadResult(index, "[" + index + "] Has been loaded"));
            }
            return null;
        }

    }
}

The above is a linear progression, meaning it's processing each item in sequence, one at a time.

Because image loading can take time and is CPU intensive process, you could make use of a ExecutorService and use a pool of threads to help spread the load.

For example:

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Many tnx, @MadProgrammer. I'll definitely give your suggestions a try to improve the image-loading. That said, it turns out this is not just an image. Further experimentation demonstrates the JList is also updating all visible elements when I select a new element. Seems like a whole lot of excess drawing is taking place whenever anything in the list changes. – mstoreysmith May 01 '21 at 13:46
  • @mstoreysmith Well, that's up to the `JList` to decide - but since we don't have any context to go by, "suggestions" are the best you're going get – MadProgrammer May 01 '21 at 21:58
1

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.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • This looks fantastic @MadProgrammer. Can't wait to take it for a spin. I'm also, still, hoping to figure out a way to override JList methods to prevent excess cell repainting, but I absolutely take your point about not invoking the fireXXX methods directly. Cheers. – mstoreysmith May 02 '21 at 13:06