JList重绘单个元素

JList repaint single element

我有一个 JList,其元素由我正在为其创建缩略图的图像文件组成(在后台线程中)。当这些缩略图可用时,我想强制重绘 just 该项目。但是,我发现当我使用 listModel 的 fireDataChanged 方法(见下文)时,all 列表中的可见项被重新绘制(使用我的自定义 ListCellRenderer)。

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

有没有办法只重绘索引项?

如果没有某种可运行的示例来说明您的问题,就不可能提出任何具体的建议。

下面的简单示例使用 SwingWorker 来更改 ListModel 中元素的值。为了让它看起来更真实,我打乱了索引的 List 并在每个索引之间应用了一个短暂的延迟。

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;
        }

    }
}

以上是线性进程,这意味着它按顺序处理每个项目,一次一个。

因为图像加载可能需要时间并且是 CPU 密集过程,您可以使用 ExecutorService 并使用线程池来帮助分散负载。

例如:

  • Make images fetched from server display in real time
  • Extracting images from a text file of 100 image urls using java

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

您不应手动调用该方法。 fireXXX(...) 方法只能由模型本身调用。

您应该使用以下方法更新模型:

model.set(...);

然后 set(...) 方法将调用适当的方法来通知 JList 重新绘制单元格。

这是我对一个简单的缩略图应用程序的尝试。它尝试通过以下方式增加性能改进:

  1. 使用默认图标加载模型,这样列表就不需要不断调整自身大小
  2. 使用 ExecutorService 以利用多个处理器
  3. 使用 ImageReader 读取文件。子采样 属性 允许您在缩放图像时使用更少的像素。

只需将 class 更改为指向包含一些 .jpg 文件的目录,然后试一试:

缩略图应用程序:

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();
       }
   }
}

缩略图渲染器

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;
    }
}

缩略图:

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();
    }
}

我在一个包含 302 个图像的目录上进行了测试。使用 ExecutorService 将加载时间从 2:31 减少到 0:35。