Search code examples
javaimageswingjlabelimageicon

Java Swing ImageIcon not showing in JLabel


As I found out the following MRE reproduces each time it is executed the same error:

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

The error, which I hope anyone can reproduce, is that the ImageIcon's image is never drawn in the shown label/frame.

As I was trying to debug it, I found out that the image load status (of the ImageIcon) is equal to MediaTracker.ABORTED which delegates to ImageObserver.ABORT of the target Component (which implements ImageObserver) of the MediaTracker of the ImageIcon instance, which in turn (according to the documentation of ImageObserver) has to do with its imageUpdate method.

I also noticed that if I call img.getWidth(null); just before setting the image to the ImageIcon then the image is drawn normally (ie the error stops reproducing) and the image load status is not equal to MediaTracker.ABORTED anymore. After reading the documentation of ImageObserver.imageUpdate method I've seen it states that the Image.getWidth(ImageObserver) method is asynchronous?...

I also noticed that if I call img.flush(); after the img.getWidth(null); call, then the error reproduces. Calling img.getWidth(null); after flushing the image stops the error from reproducing. This pattern can be repeated. As such, I come down to the probably most important piece of information I found in the documentation of 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()).

I have to note that I do care about the sequence of events. For example I need that the ImageIcon is created empty, and then use setImage to update its image. I'm saying this because, as I noticed, creating the ImageIcon and supplying it with the image at construction time seems to draw the image correctly rather than reproducing the error. After looking at the differences of the ImageIcon's constructor which accepts an Image and the ImageIcon.setImage method I've seen that the constructor also requests the property 'comment' from the image, supplying the Image.getProperty method with an ImageObserver... So I'm also guessing it's asynchronous?...

I've also called System.out.println(img.getClass()); but it ended up in a class in the sun.* package with compiled code and no documentation. So I couldn't keep up.

I hope my efforts of debugging are not missleading.

So my questions are:

  1. What causes the image to not been drawn in the MRE?
  2. How to prevent this error from happening without having to make arbitrary calls (such as img.getWidth(null); before the setImage)?

I also found this related question, but I see that the ImageIcon class internally uses a MediaTracker so what's the point of repeating the code? The reason why the MediaTracker of the ImageIcon aborts the loading of the image (according to its load status) is something I don't know.

Edit 1

A more-realistic and less-minimal MRE which still reproduces the error is the following:

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

I know I could use a SwingWorker at the button to load the image so as the user experiences no lag, but to make things simpler, I preferred to block the EDT for a little time.


Solution

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

    With your original code I was getting a NPE.

    The following seems to work. I now see a black image:

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

    Edit:

    I tried playing around with different approaches including making sure the label has an ImageObserver. Nothing worked for me.

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

    The only thing that worked was to force a repaint() on the label.