更有效地为 JList 加载图像,大概是使用 SwingWorker
more efficiently load images for JList, presumably with SwingWorker
我有一个问题希望SwingWorker能帮到我,但我不太确定如何将它集成到我的程序中。
问题:
在 CardLayout 中,我在 Card1 上有一个用于打开 Card2 的按钮。
Card2 有一个带有自定义渲染器(扩展 JLabel)的 JList,它将平均显示 1 到 6 张图像,它们是:
- PNG
- 大小约 500kb
- 随着卡片的变化通过 imageIO 加载
渲染器应用图像缩放或模糊等繁重操作,然后将图像设置为 JLabel 图标。
如果必须渲染大约 6 张图像,这几乎需要一秒钟的时间,这种情况并不经常发生,但即使是偶尔出现的无响应瞬间也让人感觉很糟糕。
现在我认为 SwingWorker 可能会有所帮助,但我对如何集成它感到非常困惑。
假设我们有这个代码片段
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Example {
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList();
private JScrollPane scroll = new JScrollPane();
private Map<String, Color> colorMap = new HashMap<>();
public Example() {
colorMap.put("red", Color.red);
colorMap.put("blue", Color.blue);
colorMap.put("cyan", Color.cyan);
colorMap.put("green", Color.green);
colorMap.put("yellow", Color.yellow);
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80,200));
list.setCellRenderer(new CustomRenderer());
DefaultListModel model = new DefaultListModel();
model.addElement(new Product("red"));
model.addElement(new Product("yellow"));
model.addElement(new Product("blue"));
model.addElement(new Product("red"));
model.addElement(new Product("cyan"));
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(new Example().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
class CustomRenderer extends JLabel implements ListCellRenderer<Product> {
private Product product;
public CustomRenderer() {
setOpaque(false);
}
@Override
public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
this.product = product;
/**
* in the actual code image is png with alpha channel respectively named to the productID of the JList object
*
* String id = product.getId();
* image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
*/
BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
Graphics g = image2.getGraphics();
/**
* this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
*/
g.drawImage(image1,0,0,null);
setIcon(new ImageIcon(image2));
return this;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(colorMap.get(product.getColor()));
g.fillRect(0,0,80,75);
}
}
class Product {
String productID;
String color;
public Product(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public String getProductID() {
return productID;
}
}
}
我是否必须从每个 getListCellRendererComponent 调用中调用 SwingWorker
接管图像操作 ?
SwingWorker 是解决这个问题的正确工具吗?
如果能帮助我加快 GUI 的这一部分,我将不胜感激。
编辑:
Hovercraft Full Of Eels 提到预加载图像可能会有所帮助,而从渲染器加载图像是根本错误的。
这引出了另一个问题:
我有一个包含大约 3000 个对象的列表(我们称它为 list1),每个对象都有一个 8kb 的 jpg 缩略图,它是通过对象 ID 加载的(也在渲染期间)
该列表同时显示大约 6 到 12 个这些缩略图(由于列表的维度)
当用户选择一个对象时,他可以按一个按钮来显示原始问题中提到的 Cardlayout 中的 Card2,它是带有对象的列表(list2)
以及非缩略图视图中的所有相关对象(500kb png + 大量图像操作)。现在我认为预加载对象的非缩略图是可行的,它是在第一个列表中选择的关系,大约有 1-6 张图像。如果我正确理解了 Hovercraft Full Of Eels 所说的内容,那么我可以在从 list1 中选择对象后使用 SwingWorker 加载这些图像。
但是 list1 的大约 3000 张图片呢,程序似乎并没有变慢或变得无响应,因为它们的尺寸相当小并且没有对缩略图进行繁重的操作,但它们仍然是从 list1 加载的渲染器。预加载几千个缩略图有意义吗?
顺便说一句。如果不希望编辑这种问题,以及是否应该将其本身作为一个问题,请随时告诉我。
would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?
不,实际上您永远不会从关键渲染方法中调用后台线程。事实上,这似乎是上面代码的主要问题——您从渲染方法中读取图像,显着降低了程序的感知响应能力。
Is SwingWorker even the right tool for this problem?
也许,但不是您考虑使用它的地方。 SwingWorker 不会加快任何速度,但通过在后台执行长 运行 任务,可以避免阻塞 Swing 事件线程,冻结 GUI。最好是读取图像一次,如果在程序启动期间未完成,则可能在 SwingWorker 中,并将它们保存到变量中。如果可以避免,请不要 每次要渲染图像时都重新读取图像。同样,不要从渲染代码中读取图像,因为这会显着降低程序的感知响应能力。
一种方法可能如下:
每当请求某个元素 (Product) 的单元格渲染器组件时,您都会检查是否已经加载了匹配的图像。如果没有,您将启动一个 Swing worker,它在后台执行加载和处理图像的工作。工作人员完成后,图像将放入缓存中供以后查找。同时,您让渲染器只说 "Loading..."
或其他内容。
一个非常的快速实现在这里:
作为 MCVE:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
public class LazyImageLoadingCellRendererTest
{
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList<Product>();
private JScrollPane scroll = new JScrollPane();
public LazyImageLoadingCellRendererTest()
{
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80, 200));
list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
LazyImageLoadingCellRendererTest::loadAndProcessImage));
DefaultListModel<Product> model = new DefaultListModel<Product>();
for (int i=0; i<1000; i++)
{
model.addElement(new Product("id" + i));
}
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(
new LazyImageLoadingCellRendererTest().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
private static final Random random = new Random(0);
private static BufferedImage loadAndProcessImage(Product product)
{
String id = product.getProductID();
int w = 100;
int h = 20;
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GREEN);
g.fillRect(0, 0, w, h);
g.setColor(Color.BLACK);
g.drawString(id, 10, 16);
g.dispose();
long delay = 500 + random.nextInt(3000);
try
{
System.out.println("Load time of " + delay + " ms for " + id);
Thread.sleep(delay);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return image;
}
class Product
{
String productID;
public Product(String productID)
{
this.productID = productID;
}
public String getProductID()
{
return productID;
}
}
}
class LazyImageLoadingCellRenderer<T> extends JLabel
implements ListCellRenderer<T>
{
private final JList<?> owner;
private final Function<? super T, ? extends BufferedImage> imageLookup;
private final Set<T> pendingImages;
private final Map<T, BufferedImage> loadedImages;
public LazyImageLoadingCellRenderer(JList<?> owner,
Function<? super T, ? extends BufferedImage> imageLookup)
{
this.owner = Objects.requireNonNull(
owner, "The owner may not be null");
this.imageLookup = Objects.requireNonNull(imageLookup,
"The imageLookup may not be null");
this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
this.pendingImages =
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
setOpaque(false);
}
class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
{
private final T element;
ImageLoadingWorker(T element)
{
this.element = element;
pendingImages.add(element);
}
@Override
protected BufferedImage doInBackground() throws Exception
{
try
{
BufferedImage image = imageLookup.apply(element);
loadedImages.put(element, image);
pendingImages.remove(element);
return image;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
@Override
protected void done()
{
owner.repaint();
}
}
@Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus)
{
BufferedImage image = loadedImages.get(value);
if (image == null)
{
if (!pendingImages.contains(value))
{
//System.out.println("Execute for " + value);
ImageLoadingWorker worker = new ImageLoadingWorker(value);
worker.execute();
}
setText("Loading...");
setIcon(null);
}
else
{
setText(null);
setIcon(new ImageIcon(image));
}
return this;
}
}
注:
这实际上只是一个显示一般方法的简单示例。当然,这可以通过多种方式进行改进。虽然实际的加载过程已经被提取到 Function
中(因此它通常适用于 任何 类型的图像,无论它来自哪里),一个主要的警告是那:它将尝试 加载 所有图像。一个不错的扩展是在此处添加一些智能,并确保它 仅 加载单元格当前可见的图像。例如,当您有一个包含 1000 个元素的列表,并且想要查看最后 10 个元素时,您不必等待加载 990 个元素。最后一个元素的优先级应 更高 并首先加载。然而,为此,可能需要稍微大一点的基础设施(主要是:一个自己的任务队列和一些与列表及其滚动窗格的更强大的连接)。 (我可能有一天会解决这个问题,因为它可能是一件美好而有趣的事情,但在那之前,上面的例子可能会做到......)
我有一个问题希望SwingWorker能帮到我,但我不太确定如何将它集成到我的程序中。
问题:
在 CardLayout 中,我在 Card1 上有一个用于打开 Card2 的按钮。 Card2 有一个带有自定义渲染器(扩展 JLabel)的 JList,它将平均显示 1 到 6 张图像,它们是:
- PNG
- 大小约 500kb
- 随着卡片的变化通过 imageIO 加载
渲染器应用图像缩放或模糊等繁重操作,然后将图像设置为 JLabel 图标。
如果必须渲染大约 6 张图像,这几乎需要一秒钟的时间,这种情况并不经常发生,但即使是偶尔出现的无响应瞬间也让人感觉很糟糕。
现在我认为 SwingWorker 可能会有所帮助,但我对如何集成它感到非常困惑。
假设我们有这个代码片段
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class Example {
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList();
private JScrollPane scroll = new JScrollPane();
private Map<String, Color> colorMap = new HashMap<>();
public Example() {
colorMap.put("red", Color.red);
colorMap.put("blue", Color.blue);
colorMap.put("cyan", Color.cyan);
colorMap.put("green", Color.green);
colorMap.put("yellow", Color.yellow);
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80,200));
list.setCellRenderer(new CustomRenderer());
DefaultListModel model = new DefaultListModel();
model.addElement(new Product("red"));
model.addElement(new Product("yellow"));
model.addElement(new Product("blue"));
model.addElement(new Product("red"));
model.addElement(new Product("cyan"));
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(new Example().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
class CustomRenderer extends JLabel implements ListCellRenderer<Product> {
private Product product;
public CustomRenderer() {
setOpaque(false);
}
@Override
public Component getListCellRendererComponent(JList<? extends Product> list, Product product, int index, boolean isSelected, boolean cellHasFocus) {
this.product = product;
/**
* in the actual code image is png with alpha channel respectively named to the productID of the JList object
*
* String id = product.getId();
* image = ImageIO.read(getClass().getResource("../../resources/images/" + id + ".png"));
*/
BufferedImage image1 = new BufferedImage(80, 50, BufferedImage.TYPE_INT_RGB);
BufferedImage image2 = new BufferedImage( 80, 75, BufferedImage.TYPE_INT_RGB);
Graphics g = image2.getGraphics();
/**
* this is only an example, in the actual code I might also apply gaussian blurs or rescale several time
*/
g.drawImage(image1,0,0,null);
setIcon(new ImageIcon(image2));
return this;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(colorMap.get(product.getColor()));
g.fillRect(0,0,80,75);
}
}
class Product {
String productID;
String color;
public Product(String color) {
this.color = color;
}
public String getColor() {
return color;
}
public String getProductID() {
return productID;
}
}
}
我是否必须从每个 getListCellRendererComponent 调用中调用 SwingWorker 接管图像操作 ?
SwingWorker 是解决这个问题的正确工具吗?
如果能帮助我加快 GUI 的这一部分,我将不胜感激。
编辑: Hovercraft Full Of Eels 提到预加载图像可能会有所帮助,而从渲染器加载图像是根本错误的。
这引出了另一个问题:
我有一个包含大约 3000 个对象的列表(我们称它为 list1),每个对象都有一个 8kb 的 jpg 缩略图,它是通过对象 ID 加载的(也在渲染期间) 该列表同时显示大约 6 到 12 个这些缩略图(由于列表的维度)
当用户选择一个对象时,他可以按一个按钮来显示原始问题中提到的 Cardlayout 中的 Card2,它是带有对象的列表(list2) 以及非缩略图视图中的所有相关对象(500kb png + 大量图像操作)。现在我认为预加载对象的非缩略图是可行的,它是在第一个列表中选择的关系,大约有 1-6 张图像。如果我正确理解了 Hovercraft Full Of Eels 所说的内容,那么我可以在从 list1 中选择对象后使用 SwingWorker 加载这些图像。
但是 list1 的大约 3000 张图片呢,程序似乎并没有变慢或变得无响应,因为它们的尺寸相当小并且没有对缩略图进行繁重的操作,但它们仍然是从 list1 加载的渲染器。预加载几千个缩略图有意义吗?
顺便说一句。如果不希望编辑这种问题,以及是否应该将其本身作为一个问题,请随时告诉我。
would I have to call a SwingWorker from every getListCellRendererComponent call to take over the image operations ?
不,实际上您永远不会从关键渲染方法中调用后台线程。事实上,这似乎是上面代码的主要问题——您从渲染方法中读取图像,显着降低了程序的感知响应能力。
Is SwingWorker even the right tool for this problem?
也许,但不是您考虑使用它的地方。 SwingWorker 不会加快任何速度,但通过在后台执行长 运行 任务,可以避免阻塞 Swing 事件线程,冻结 GUI。最好是读取图像一次,如果在程序启动期间未完成,则可能在 SwingWorker 中,并将它们保存到变量中。如果可以避免,请不要 每次要渲染图像时都重新读取图像。同样,不要从渲染代码中读取图像,因为这会显着降低程序的感知响应能力。
一种方法可能如下:
每当请求某个元素 (Product) 的单元格渲染器组件时,您都会检查是否已经加载了匹配的图像。如果没有,您将启动一个 Swing worker,它在后台执行加载和处理图像的工作。工作人员完成后,图像将放入缓存中供以后查找。同时,您让渲染器只说 "Loading..."
或其他内容。
一个非常的快速实现在这里:
作为 MCVE:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ListCellRenderer;
import javax.swing.SwingWorker;
public class LazyImageLoadingCellRendererTest
{
private JPanel mainPanel = new JPanel();
private JList<Product> list = new JList<Product>();
private JScrollPane scroll = new JScrollPane();
public LazyImageLoadingCellRendererTest()
{
mainPanel.setBackground(new Color(129, 133, 142));
scroll.setViewportView(list);
scroll.setHorizontalScrollBarPolicy(
JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scroll.setPreferredSize(new Dimension(80, 200));
list.setCellRenderer(new LazyImageLoadingCellRenderer<Product>(list,
LazyImageLoadingCellRendererTest::loadAndProcessImage));
DefaultListModel<Product> model = new DefaultListModel<Product>();
for (int i=0; i<1000; i++)
{
model.addElement(new Product("id" + i));
}
list.setModel(model);
mainPanel.add(scroll);
}
public static void main(String[] args) throws IOException
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
JFrame frame = new JFrame("WorkerTest");
frame.setContentPane(
new LazyImageLoadingCellRendererTest().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocation(300, 300);
frame.setMinimumSize(new Dimension(160, 255));
frame.setVisible(true);
}
});
}
private static final Random random = new Random(0);
private static BufferedImage loadAndProcessImage(Product product)
{
String id = product.getProductID();
int w = 100;
int h = 20;
BufferedImage image =
new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.GREEN);
g.fillRect(0, 0, w, h);
g.setColor(Color.BLACK);
g.drawString(id, 10, 16);
g.dispose();
long delay = 500 + random.nextInt(3000);
try
{
System.out.println("Load time of " + delay + " ms for " + id);
Thread.sleep(delay);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return image;
}
class Product
{
String productID;
public Product(String productID)
{
this.productID = productID;
}
public String getProductID()
{
return productID;
}
}
}
class LazyImageLoadingCellRenderer<T> extends JLabel
implements ListCellRenderer<T>
{
private final JList<?> owner;
private final Function<? super T, ? extends BufferedImage> imageLookup;
private final Set<T> pendingImages;
private final Map<T, BufferedImage> loadedImages;
public LazyImageLoadingCellRenderer(JList<?> owner,
Function<? super T, ? extends BufferedImage> imageLookup)
{
this.owner = Objects.requireNonNull(
owner, "The owner may not be null");
this.imageLookup = Objects.requireNonNull(imageLookup,
"The imageLookup may not be null");
this.loadedImages = new ConcurrentHashMap<T, BufferedImage>();
this.pendingImages =
Collections.newSetFromMap(new ConcurrentHashMap<T, Boolean>());
setOpaque(false);
}
class ImageLoadingWorker extends SwingWorker<BufferedImage, Void>
{
private final T element;
ImageLoadingWorker(T element)
{
this.element = element;
pendingImages.add(element);
}
@Override
protected BufferedImage doInBackground() throws Exception
{
try
{
BufferedImage image = imageLookup.apply(element);
loadedImages.put(element, image);
pendingImages.remove(element);
return image;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
@Override
protected void done()
{
owner.repaint();
}
}
@Override
public Component getListCellRendererComponent(JList<? extends T> list,
T value, int index, boolean isSelected, boolean cellHasFocus)
{
BufferedImage image = loadedImages.get(value);
if (image == null)
{
if (!pendingImages.contains(value))
{
//System.out.println("Execute for " + value);
ImageLoadingWorker worker = new ImageLoadingWorker(value);
worker.execute();
}
setText("Loading...");
setIcon(null);
}
else
{
setText(null);
setIcon(new ImageIcon(image));
}
return this;
}
}
注:
这实际上只是一个显示一般方法的简单示例。当然,这可以通过多种方式进行改进。虽然实际的加载过程已经被提取到 Function
中(因此它通常适用于 任何 类型的图像,无论它来自哪里),一个主要的警告是那:它将尝试 加载 所有图像。一个不错的扩展是在此处添加一些智能,并确保它 仅 加载单元格当前可见的图像。例如,当您有一个包含 1000 个元素的列表,并且想要查看最后 10 个元素时,您不必等待加载 990 个元素。最后一个元素的优先级应 更高 并首先加载。然而,为此,可能需要稍微大一点的基础设施(主要是:一个自己的任务队列和一些与列表及其滚动窗格的更强大的连接)。 (我可能有一天会解决这个问题,因为它可能是一件美好而有趣的事情,但在那之前,上面的例子可能会做到......)