Krzysztof Majewski
Krzysztof Majewski

Reputation: 2534

Resizing Image in JPanel

I am tryimg to set an image as background in JPanel and resize it to desired size.

This is MyPanel where i choose image and set it as backgorund:

public class MyPanel extends JPanel {

    Image img;

    public MyPanel(LayoutManager l) {
        super(l);

        JFileChooser fc = new JFileChooser();
        int result = fc.showOpenDialog(null);
        if (result == JFileChooser.APPROVE_OPTION) {
            File file = fc.getSelectedFile();
            String sname = file.getAbsolutePath();
            img = new ImageIcon(sname).getImage();

            double xRatio = img.getWidth(null) / 400;
            double yRatio = img.getHeight(null) / 400;
            double ratio = (xRatio + yRatio) / 2;

            img = img.getScaledInstance((int)(img.getWidth(null) / ratio), (int)(img.getHeight(null) / ratio), Image.SCALE_SMOOTH);
        }

        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        g.drawImage(img, 0, 0, Color.WHITE, null);
    }
}

And this is my frame:

public class MyFrame extends JFrame {

    public MyFrame () {

        initUI();
    }

    private void initUI() {

        MyPanel pnl = new MyPanel(null);
        add(pnl);

        setSize(600, 600);
        setTitle("My component");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                MyFrame ex = new MyFrame ();
                ex.setVisible(true);
            }
        });
    }
}

The problem is the image doesn't show at first. It shows when i for example change frame size a little.

Like that:

enter image description here

Upvotes: 3

Views: 1611

Answers (2)

VGR
VGR

Reputation: 44414

You are loading your image asynchronously.

The ImageIcon(String) constructor uses Toolkit.getImage internally, which is a hold-over from the 1990s, when many home Internet connections were so slow that it made sense to always load images in a background thread.

Since the image is loading in the background, img.getWidth(null) might return the image's size, or it might return -1. The documentation explains this.

So, how do you wait until the image has been loaded in that background thread?

Normally, you would observe the image's progress, using an ImageObserver. It just so happens that all AWT Components, and by extension, all Swing JComponents, implement ImageObserver. So you have an ImageObserver object: your MyPanel instance.

So, instead of passing null to all those methods, you would pass your MyPanel instance—that is, this:

double xRatio = img.getWidth(this) / 400;
double yRatio = img.getHeight(this) / 400;

And:

g.drawImage(img, 0, 0, Color.WHITE, this);

However… this will properly track the image's loading progress, but still doesn't guarantee that img.getWidth will return a positive value at the time that you call it.

To guarantee that an image is fully loaded immediately, you can replace the use of ImageIcon with ImageIO.read:

File file = fc.getSelectedFile();
try {
    img = ImageIO.read(file);
} catch (IOException e) {
    throw new RuntimeException("Could not load \"" + file + "\"", e);
}

This is different from using ImageIcon and Toolkit, because it doesn't just return an Image, it returns a BufferedImage, which is a type of Image that is guaranteed to be fully loaded and present in memory—no background loading to worry about.

Since a BufferedImage is already loaded, it has some additional methods which don't need an ImageObserver, in particular getWidth and getHeight methods which don't require an argument:

BufferedImage bufferedImg = (BufferedImage) img;
double xRatio = bufferedImg.getWidth() / 400.0;
double yRatio = bufferedImg.getHeight() / 400.0;

Notice I changed 400 to 400.0. This is because dividing one int by another int results in integer arithmetic in Java. For instance, 200 / 400 returns zero, because 200 and 400 are both int values. 5 / 2 produces the int value 2.

However, if either or both numbers are doubles, Java treats the entire thing as a double expression. So, (double) 200 / 400 returns 0.5. The presence of a decimal point in a sequence of digits indicates a double value, so 200.0 / 400 and 200 / 400.0 (and, of course, 200.0 / 400.0) will also return 0.5.

Finally, there is the issue of scaling. I recommend reading MadProgrammer's answer, and in particular, the java.net article to which his answer links, The Perils of Image.getScaledInstance().

The short version is that Image.getScaledInstance is another hold-over from the 1990s, and doesn't do a very good job of scaling. There are two better options:

  1. Draw your image into a new image, and let the drawImage method handle the scaling, using the Graphics2D object's RenderingHints.
  2. Use an AffineTransformOp on your image to create a new, scaled image.

Method 1:

BufferedImage scaledImage = new BufferedImage(
    img.getColorModel(),
    img.getRaster().createCompatibleWritableRaster(newWidth, newHeight),
    false, new Properties());

Graphics g = scaledImage.createGraphics();
g.setRenderingHint(RenderingHints.KEY_RENDERING,
                   RenderingHints.VALUE_RENDER_SPEED);
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                   RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);

g.drawImage(img, 0, 0, newWidth, newHeight, null);
g.dispose();

Method 2:

RenderingHints hints = new RenderingHints();
hints.put(RenderingHints.KEY_RENDERING,
          RenderingHints.VALUE_RENDER_SPEED);
hints.put(RenderingHints.KEY_INTERPOLATION,
          RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);

AffineTransform transform = AffineTransform.getScaleInstance(
    (double) newWidth / img.getWidth(),
    (double) newHeight / img.getHeight());
BufferedImageOp op = new AffineTransformOp(transform, hints);

BufferedImage scaledImage = op.filter(img, null);

You may want to alter the RenderingHints values, based on your own preferred trade-off of speed versus quality. They're all documented in the RenderingHints class.

Upvotes: 2

Murat Karagöz
Murat Karagöz

Reputation: 37604

You have to invoke setVisible(true):

public MyFrame () {    
    initUI();
    setVisible(true);
}

Upvotes: 0

Related Questions