Java Swing ImageIcon 未在 JLabel 中显示

Java Swing ImageIcon not showing in JLabel

我发现以下 MRE 每次执行时都会重现相同的错误:

import java.awt.Component;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.image.BufferedImage;
import java.util.Objects;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class IconDelegate implements Icon {
    private final Icon delegated;

    public IconDelegate(final Icon delegated) {
        this.delegated = Objects.requireNonNull(delegated);
    }

    @Override
    public int getIconWidth() {
        return delegated.getIconWidth();
    }

    @Override
    public int getIconHeight() {
        return delegated.getIconHeight();
    }

    @Override
    public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
        delegated.paintIcon(c, g, x, y);
        //Some other drawing operations would be here...
    }

    public static void main(final String[] args) {
        final ImageIcon ii = new ImageIcon();
        final GraphicsEnvironment genv = GraphicsEnvironment.getLocalGraphicsEnvironment();
        final GraphicsDevice gdev = genv.getDefaultScreenDevice();
        final GraphicsConfiguration gcnf = gdev.getDefaultConfiguration();
        final BufferedImage bimg = gcnf.createCompatibleImage(300, 400);
        final Image img = bimg.getScaledInstance(150, 200, Image.SCALE_SMOOTH);
        //img.getWidth(null); //Uncomment this line and the image will be drawn normally!
        ////img.flush(); //Uncomment this line and the image will not be drawn.
        ii.setImage(img);
        System.out.println(ii.getImageLoadStatus() == MediaTracker.ABORTED);
        final JLabel lbl = new JLabel(new IconDelegate(ii));
        final JFrame frame = new JFrame("ImageIcon delegate.");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(lbl);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}

我希望任何人都可以重现的错误是 ImageIcon 的图像从未在显示的 label/frame 中绘制。

当我尝试调试它时,我发现 图像加载状态 ImageIcon 的)等于 MediaTracker.ABORTED 委托到 ImageIcon 实例的 MediaTracker 的目标 Component(实现 ImageObserver)的 ImageObserver.ABORT,后者又(根据 ImageObserver) 与其 imageUpdate 方法有关。

我还注意到,如果我在将图像设置为 ImageIcon 之前调用 img.getWidth(null);,则图像会正常绘制(即错误停止再现)并且图像加载状态为 不再等于MediaTracker.ABORTED。 在阅读 ImageObserver.imageUpdate 方法的文档后,我看到它指出 Image.getWidth(ImageObserver) 方法是 asynchronous?...

我还注意到,如果我在 img.getWidth(null); 调用之后调用 img.flush();,则会重现错误。 刷新图像后调用 img.getWidth(null); 会停止重现错误。 这种模式可以重复。 因此,我归结为我在 MediaTracker:

的文档中找到的可能最重要的信息

If no ImageObservers are observing the image when the first frame has finished loading, the image might flush itself to conserve resources (see Image.flush()).

我必须指出,我确实关心事件的顺序。例如,我需要将 ImageIcon 创建为空,然后使用 setImage 更新其图像。 我这样说是因为,正如我注意到的那样,创建 ImageIcon 并在构建时为其提供图像似乎可以正确绘制图像而不是重现错误。 在查看了接受 ImageImageIcon.setImage 方法的 ImageIcon 构造函数的差异后,我发现构造函数还请求 属性 'comment' 从图像中,为 Image.getProperty 方法提供 ImageObserver... 所以我也猜它是 异步 ? ...

我也调用了 System.out.println(img.getClass());,但它最终出现在 sun.* 包中的 class 中,带有 编译代码 并且没有文档.所以我跟不上。

希望我的调试工作没有误导。

所以我的问题是:

  1. MRE 中无法绘制图像的原因是什么?
  2. 如何在不必进行任意调用(例如 setImage 之前的 img.getWidth(null);)的情况下防止此错误发生?

我也发现了 this 相关问题,但我看到 ImageIcon class 内部使用了 MediaTracker 那么重复代码有什么意义呢? ImageIconMediaTracker 中止图像加载的原因(根据其加载状态)我不知道。

编辑 1

一个更真实和更简单的 MRE 仍然重现错误如下:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.GridBagLayout;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Objects;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class ImageThumbnail implements Icon {
    private final ImageIcon delegated;
    private final int w, h;
    private String text;

    public ImageThumbnail(final int width, final int height, final String text) {
        w = width;
        h = height;
        this.text = Objects.toString(text).trim();
        delegated = new ImageIcon();
    }

    @Override
    public int getIconWidth() {
        return w;
    }

    @Override
    public int getIconHeight() {
        return h;
    }

    public void setText(final String text) {
        this.text = Objects.toString(text).trim();
    }

    //Scales the given image to fit this icon's width and height maintaining aspect ratio:
    public void setImage(final BufferedImage bimg) {
        final int bimgw = bimg.getWidth(),
                  bimgh = bimg.getHeight();
        if (bimgw > bimgh)
            delegated.setImage(bimg.getScaledInstance(Math.min(w, bimgw), -1, Image.SCALE_SMOOTH));
        else
            delegated.setImage(bimg.getScaledInstance(-1, Math.min(h, bimgh), Image.SCALE_SMOOTH));
        //Alternatively you can try the following code if you don't trust the -1 arguments above:
        //delegated.setImage(bimg.getScaledInstance(w, h, Image.SCALE_SMOOTH));
    }

    @Override
    public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
        delegated.paintIcon(c, g, x, y);
        //Other drawing operations here, like drawing a String, at the center, over the ImageIcon:
        g.drawString(text, x + w / 2 - g.getFontMetrics().stringWidth(text) / 2, y + h / 2);
    }

    private static JPanel centered(final Component c) {
        /*Creating a JPanel with GridBagLayout and a single component
        in it, will layout the component in the center of the JPanel.*/
        final JPanel panel = new JPanel(new GridBagLayout());
        panel.add(c);
        return panel;
    }

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {

            final ImageThumbnail thumb = new ImageThumbnail(200, 200, "Select an image...");

            final JLabel lbl = new JLabel(thumb);

            final JFileChooser chooser = new JFileChooser();
            chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
            chooser.setMultiSelectionEnabled(false);

            final JButton load = new JButton("Load image");
            load.addActionListener(e -> {
                if (chooser.showOpenDialog(load) == JFileChooser.APPROVE_OPTION) {
                    try {
                        thumb.setImage(ImageIO.read(chooser.getSelectedFile()));
                        thumb.setText("Image-001");
                        lbl.setForeground(Color.GREEN.darker());
                        //lbl.repaint(); //Not needed, as I call 'lbl.setForeground(Color.GREEN.darker());' above...
                    }
                    catch (final IOException iox) {
                        iox.printStackTrace();
                    }
                }
            });

            final JPanel contents = new JPanel(new BorderLayout());
            contents.add(centered(lbl), BorderLayout.CENTER);
            contents.add(centered(load), BorderLayout.PAGE_END);

            final JFrame frame = new JFrame("Thumbnail of images.");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(contents);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

我知道我可以在按钮上使用 SwingWorker 来加载图像,这样用户就不会体验到延迟,但为了让事情更简单,我更愿意将 EDT 屏蔽一段时间。

How to prevent this error from happening without having to make arbitrary calls

用你的原始代码我得到了一个 NPE。

以下似乎可行。我现在看到黑色图像:

//final ImageIcon ii = new ImageIcon();
…
//ii.setImage(img);
ImageIcon ii = new ImageIcon( img );

编辑:

我尝试了不同的方法,包括确保标签有一个 ImageObserver。对我没有任何作用。

import java.awt.*;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.image.*;

public class IconMRE extends JPanel
{
    private ImageIcon icon = new ImageIcon( new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB) );
    private JLabel label1 = new JLabel( icon );

    public IconMRE()
    {
        setLayout( new BorderLayout(20, 20) );

        label1 = new JLabel( icon );
        add(label1, BorderLayout.LINE_START);

        JLabel label2 = new JLabel();
        icon.setImageObserver( label2 );
        label2.setIcon( icon );
        add(label2, BorderLayout.LINE_END);


        JComboBox<Color>comboBox = new JComboBox<Color>();
        comboBox.addItem( Color.RED );
        comboBox.addItem( Color.GREEN );
        comboBox.addItem( Color.BLUE );
        add(comboBox, BorderLayout.PAGE_START);

        comboBox.addActionListener( (e) ->
        {
            Color background = (Color)comboBox.getSelectedItem();
            BufferedImage image = new BufferedImage(200, 200, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2d = image.createGraphics();
            g2d.setColor( background );
            g2d.fillRect(0, 0, 200, 200);
            g2d.dispose();
//          image.getWidth(null);
            icon.setImage( image );
//          label1.repaint();
        });

        comboBox.setSelectedIndex(2);
    }

    private static void createAndShowUI()
    {
        JFrame frame = new JFrame("IconMRE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( new IconMRE() );
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowUI();
            }
        });
    }
}

唯一有效的方法是在标签上强制重绘()。