【问题标题】:JList repaint single elementJList重绘单个元素
【发布时间】:2021-07-24 05:47:04
【问题描述】:

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

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

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

【问题讨论】:

  • 加载缩略图后元素的大小是否会发生变化 - 强制重新布局?
  • 老实说,您不应该以这种方式调用fireContentsChange - 它确实应该是protected 方法(恕我直言)。您可能会考虑尝试删除并重新插入该元素?
  • 填充空白图标,大小正确,然后在创建图像后将带有图像的图标插入正确的模型位置。 SwingWorker 发布/进程对在这里可以很好地工作,不是吗?
  • 元素大小是硬编码的,所以不会改变。
  • “我想强制重绘只是该项目。” 对我来说,这听起来像是过早的优化。 一般提示: 1) 为尽快获得更好的帮助,edit 添加minimal reproducible exampleShort, Self Contained, Correct Example。 2) 例如,获取图像的一种方法是热链接到在this Q&A 中看到的图像。例如。 this answer 中的代码热链接到嵌入在this question 中的图像。

标签: java swing jlist repaint


【解决方案1】:

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

以下简单示例使用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 并使用线程池来帮助分散负载。

例如:

【讨论】:

  • 许多 tnx,@MadProgrammer。我一定会尝试改进图像加载的建议。也就是说,事实证明这不仅仅是一个图像。进一步的实验表明,当我选择一个新元素时,JList 也会更新所有可见元素。每当列表中的任何内容发生变化时,似乎都会发生大量多余的绘图。
  • @mstoreysmith 嗯,这取决于 JList 来决定 - 但由于我们没有任何上下文可参考,“建议”是您将获得的最佳选择
【解决方案2】:

我发现当我使用listModel的fireDataChanged方法(见下文)时,列表中的所有可见项都被重新绘制了

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

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

model.set(...);

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

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

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

只需将类更改为指向包含一些 .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。

【讨论】:

  • 这看起来很棒@MadProgrammer。迫不及待想试一试。我也仍然希望找到一种方法来覆盖 JList 方法以防止过多的单元格重绘,但我绝对同意您关于不直接调用 fireXXX 方法的观点。干杯。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-13
  • 1970-01-01
  • 2023-03-18
  • 1970-01-01
  • 2013-04-19
  • 1970-01-01
相关资源
最近更新 更多