Reputation: 2534
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:
Upvotes: 3
Views: 1611
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:
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
Reputation: 37604
You have to invoke setVisible(true)
:
public MyFrame () {
initUI();
setVisible(true);
}
Upvotes: 0