~ Dogstopper
In my first tutorial, I domonstrated how you can load images from both files and from the web, and in my second tutorial I demonstrated how to scale images so that they grew as you increase the height, but what I neglected to tell you was that often the image could grow to large width-wise and would dissapear off the edge of the screen. In this tutorial, I will go over the basic math needed to completely center an image, so that no part of the image will go offscreen. Also, in this tutorial I will show you how to load images in a separate thread safely even though Swing is not thread-safe.
Centering An Image - A Simple Ratio
When truly scaling and centering an image, you must remember that it must compensate so that the greatest dimension is always inside the boundaries. For this calculation, you need the scaledWidth and scaledHeight. To get those, its really a simple proportion. Let's start with scaledWidth.
Given: Big Image Width - img.getWidth()
Big Image Height - img.getHeight()
The Maximum Possible Height - getHeight()
Goal: To find the maximum possible width of the scaled image
img.getWidth() / img.getHeight() = x / getHeight()
Now, just cross multiply.
(img.getWidth() / img.getHeight()) * getHeight() = x
So, that's how we get the scaled image to fit into a maximum height. Now how about to fit an image into the maximum width (In our previous example, when the image is too wide, but fits vertically). For this, we need to scale the height.
Given: Big Image Width - img.getWidth()
Big Image Height - img.getHeight()
The Maximum Possible Width - getWidth()
Goal: To find the maximum possible height of the scaled image
img.getHeight() / img.getWidth() = x / getWidth()
Now, just cross multiply.
(img.getHeight() / img.getWidth()) * getWidth() = x
If all this math doesn't make much sense, it will after updationg the PicturePanel. If you've been following previous examples, you should know how exactly to set up this PicturePanel. I'm just going to rewrite the paintComponent() method to demonstrate this new technique. If it helps, draw out on a sheet of paper a centered image just so that you can visualize it. Remeber, this code will scale down a large image and scale up a small image.
public void paintComponent(Graphics g) {
super.paintComponent(g);
// Scale it by width
int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
// If the image is not off the screen horizontally...
if (scaledWidth < getWidth()) {
// Center the left and right destination x coordinates.
int leftOffset = getWidth()/2 - scaledWidth/2;
int rightOffset = getWidth()/2 + scaledWidth/2;
g.drawImage(img,
leftOffset, 0, rightOffset, getHeight(),
0, 0, img.getWidth(), img.getHeight(),
null);
}
// Otherwise, the image width is too much, even scaled
// So we need to center it the other direction
else {
int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
int topOffset = getHeight()/2 - scaledHeight/2;
int bottomOffset = getHeight()/2 + scaledHeight/2;
g.drawImage(img,
0, topOffset, getWidth(), bottomOffset,
0, 0, img.getWidth(), img.getHeight(),
null);
}
}
If at this point, you are still confused as to what this does, fire up your compiler and stretch the JFrame to the extreme in both directions...You'll find that the image never leaves the frame.
Loading Images In Separate Threads
If you have ever made a GUI application with threads, it was wrong because Swing is not thread safe. However, there must be some way to have a multithreaded GUI application! Well, yes, sort of. Let me introduce you to SwingWorker, an object that allows you to load an object in the background while other aspects of the program continue, unlike our first tutorial where the application would lock up until it was loaded. Here is what the Swing Worker looks like:
// The BufferedImage specifies that the doInBackground() method returns a BufferedImage
// And the Void specifies that the done() method is void.
private SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {
@Override
public BufferedImage doInBackground() {
// All we do in the background is load like we did in tutorial 1
BufferedImage image = null;
try {
image = ImageIO.read(url);
} catch (IOException e) {
System.err.println("A loading error occurred");
}
return image;
}
// This method is called when the doInBackground() method is done
@Override
public void done() {
try {
// We are going to set a global img variable to
// the loaded image.
img = get();
// Catch any errors
} catch (InterruptedException ignore) {}
catch (ExecutionException e) {
System.err.println("A loading error occurred: " + why);
}
}
};
Now, your question may be why use the get() method to set the img variable? Well, that is to absolutely ensure that the image is done loading and the thread is done executing. Otherwise you can get extreme synchronization errors. So, I made a helper class to hold this SwingWorker. Just think of get() as the way to access what was loaded in doInBackground().
public class WebScanner {
// These are the variables that hold the URL and the
// image when done loading
private BufferedImage img;
private URL url;
// This is a bit more complicated, but I explained it above.
private SwingWorker<BufferedImage, Void> worker = new SwingWorker<BufferedImage, Void>() {
@Override
public BufferedImage doInBackground() {
BufferedImage image = null;
try {
image = ImageIO.read(url);
} catch (IOException e) {
System.err.println("A loading error occurred");
}
return image;
}
@Override
public void done() {
try {
img = get();
} catch (InterruptedException ignore) {}
catch (ExecutionException e) {
String why = null;
Throwable cause = e.getCause();
if (cause != null) {
why = cause.getMessage();
} else {
why = e.getMessage();
}
System.err.println("A loading error occurred: " + why);
}
}
};
// Sets the URL. Again...using an astronomy picture...
public void setURL() {
try {
url = new URL("http://antwrp.gsfc.nasa.gov/apod/image/1003/mb_2010-03-10_SeaGullThor.jpg");
} catch (MalformedURLException e) {
System.err.println("Your tutorial writer wrote down the wrong internet address");
}
}
// This returns the image when done loading. If there is
// a null image, then we retry.
public BufferedImage getImage(String fn) {
if (img == null) {
System.err.println("Attempting again");
// Starts the worker
worker.execute();
}
return img;
}
// Access to the worker
public void startWorker() {
// Starts the worker
worker.execute();
}
public boolean isWorkerDone() {
return worker.isDone();
}
}
This class helps the SwingWorker do what it needs to do, whether it be starting it or checking whether it's done. Also, we have a method that grabs the global img variable. Now, I created a GUI that ha a splitPane to allow the image to be loading and to have a place to let the user know wha's going on with the image. Like above, I'm going to explain little here, but comment throughout the code.
public class WebFrame extends JFrame implements ActionListener {
private PicturePanel imagePanel;
private WebScanner scan;
private Timer timer;
private JTextArea textField;
private JSplitPane splitPane;
public WebFrame() {
super("Picture Viewer and Downloader");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(900,600);
// Set our initial properties for our text output field
textField = new JTextArea("Please wait...Loading huge image");
textField.setLineWrap(true);
JScrollPane pane = new JScrollPane(textField);
// Instantiate our picture panel
imagePanel = new PicturePanel();
// Add the textField and imagePanel to the splitPane
splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT,
imagePanel, pane);
splitPane.setOneTouchExpandable(true);
splitPane.setDividerLocation(540);
// Add the splitPane to the Frame and make frame visible
add(splitPane);
setVisible(true);
// Make a new WebScanner and set the loading url.
scan = new WebScanner();
scan.setURL();
// Make a timer that execute every half second.
timer = new Timer(500, this);
timer.setInitialDelay(1000);
timer.start();
// This actually begins our worker thread
scan.startWorker();
// Let the user know what's going on.
textField.setText("Still working");
}
@Override
public void actionPerformed(ActionEvent e) {
// If the worker is not done, then add a dot and return
if (!scan.isWorkerDone()) {
textField.append(".");
return;
}
// If the image is null, then keep trying...
BufferedImage img = scan.getImage("");
if (img == null) {
textField.append(".");
return;
}
// Finally, if the image is loaded, set it and stop the timer.
imagePanel.setImage(scan.getImage(""));
textField.setText("Image loaded");
timer.stop();
}
public static void main(String[] args) {
new WebFrame();
}
}
See how we use the WebScanner class to check up on the SwuingWorker? In the actionPerformed() method (called every 1/2 second by the timer), we check to see if the image is done loading and if so, then we stop the timer and set the image in the image panel, otherwise we wait for it to finish. Now, the PicturePanel class is pretty similar to what you had before, but it has centered images and a "busy screen" that spins a line around and around until the iamge is done loading. (The line spinning is accomplished using the point (cos theta, sin theta) from the center). I again comment in there.
public class PicturePanel extends JPanel implements ActionListener {
private static final long serialVersionUID = -8687907176608557245L;
// this is our image.
private BufferedImage img = null;
// Used for the "busy screen"
private double theta = 0;
// Start the timer that repaints every 60 milliseconds.
// If the image is loaded, it displays the image, otherwise
// it continues the busy screen.
public PicturePanel() {
Timer time = new Timer(60, this);
time.start();
}
@Override
public void actionPerformed(ActionEvent arg0) {
repaint();
}
// This sets the loaded image, which stops the "busy screen"
public void setImage(BufferedImage image) {
img = image;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// If the image is loaded.
if (img != null) {
// Then we center it on the screen as I explained above.
int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
if (scaledWidth < getWidth()) {
// Center the left and right destination x coordinates.
int leftOffset = getWidth()/2 - scaledWidth/2;
int rightOffset = getWidth()/2 + scaledWidth/2;
g.drawImage(img,
leftOffset, 0, rightOffset, getHeight(),
0, 0, img.getWidth(), img.getHeight(),
null);
}
else {
int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
int topOffset = getHeight()/2 - scaledHeight/2;
int bottomOffset = getHeight()/2 + scaledHeight/2;
g.drawImage(img,
0, topOffset, getWidth(), bottomOffset,
0, 0, img.getWidth(), img.getHeight(),
null);
}
}
// However, if the image has not been loaded yet, then
// we make a busy screen (a line that swings in a circle).
else {
// set the origin to the center of the screen.
int originX = getWidth()/2;
int originY = getHeight()/2;
// Trig to make the line move along a circle.
theta += Math.PI/12;
g.setColor(Color.RED);
g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()),
(int)(Math.sin(theta)*getHeight()));
}
}
}
If you don't understand the trig, then that's simply what's making the line spin as you will see when you run it.
Alright, well that's all for this tutorial. I hope you understand how to use SwingWorkers to make SAFE multithreaded GUI applications.
Oh, Here's all three classes with imports in case you missed something:
WebScanner.java
public class PicturePanel extends JPanel implements ActionListener {
private static final long serialVersionUID = -8687907176608557245L;
// this is our image.
private BufferedImage img = null;
// Used for the "busy screen"
private double theta = 0;
// Start the timer that repaints every 60 milliseconds.
// If the image is loaded, it displays the image, otherwise
// it continues the busy screen.
public PicturePanel() {
Timer time = new Timer(60, this);
time.start();
}
@Override
public void actionPerformed(ActionEvent arg0) {
repaint();
}
// This sets the loaded image, which stops the "busy screen"
public void setImage(BufferedImage image) {
img = image;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// If the image is loaded.
if (img != null) {
// Then we center it on the screen as I explained above.
int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
if (scaledWidth < getWidth()) {
// Center the left and right destination x coordinates.
int leftOffset = getWidth()/2 - scaledWidth/2;
int rightOffset = getWidth()/2 + scaledWidth/2;
g.drawImage(img,
leftOffset, 0, rightOffset, getHeight(),
0, 0, img.getWidth(), img.getHeight(),
null);
}
else {
int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
int topOffset = getHeight()/2 - scaledHeight/2;
int bottomOffset = getHeight()/2 + scaledHeight/2;
g.drawImage(img,
0, topOffset, getWidth(), bottomOffset,
0, 0, img.getWidth(), img.getHeight(),
null);
}
}
// However, if the image has not been loaded yet, then
// we make a busy screen (a line that swings in a circle.
else {
int originX = getWidth()/2;
int originY = getHeight()/2;
// Trig to make the line move along a circle.
theta += Math.PI/12;
g.setColor(Color.RED);
g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()),
(int)(Math.sin(theta)*getHeight()));
}
}
}
PicturePanel.java
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import javax.swing.JPanel;
import javax.swing.Timer;
public class PicturePanel extends JPanel implements ActionListener {
private static final long serialVersionUID = -8687907176608557245L;
// this is our image.
private BufferedImage img = null;
// Used for the "busy screen"
private double theta = 0;
// Start the timer that repaints every 60 milliseconds.
// If the image is loaded, it displays the image, otherwise
// it continues the busy screen.
public PicturePanel() {
Timer time = new Timer(60, this);
time.start();
}
@Override
public void actionPerformed(ActionEvent arg0) {
repaint();
}
// This sets the loaded image, which stops the "busy screen"
public void setImage(BufferedImage image) {
img = image;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// If the image is loaded.
if (img != null) {
// Then we center it on the screen as I explained above.
int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
if (scaledWidth < getWidth()) {
// Center the left and right destination x coordinates.
int leftOffset = getWidth()/2 - scaledWidth/2;
int rightOffset = getWidth()/2 + scaledWidth/2;
g.drawImage(img,
leftOffset, 0, rightOffset, getHeight(),
0, 0, img.getWidth(), img.getHeight(),
null);
}
else {
int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
int topOffset = getHeight()/2 - scaledHeight/2;
int bottomOffset = getHeight()/2 + scaledHeight/2;
g.drawImage(img,
0, topOffset, getWidth(), bottomOffset,
0, 0, img.getWidth(), img.getHeight(),
null);
}
}
// However, if the image has not been loaded yet, then
// we make a busy screen (a line that swings in a circle.
else {
int originX = getWidth()/2;
int originY = getHeight()/2;
// Trig to make the line move along a circle.
theta += Math.PI/12;
g.setColor(Color.RED);
g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()),
(int)(Math.sin(theta)*getHeight()));
}
}
}
WebFrame.java
public class PicturePanel extends JPanel implements ActionListener {
private static final long serialVersionUID = -8687907176608557245L;
// this is our image.
private BufferedImage img = null;
// Used for the "busy screen"
private double theta = 0;
// Start the timer that repaints every 60 milliseconds.
// If the image is loaded, it displays the image, otherwise
// it continues the busy screen.
public PicturePanel() {
Timer time = new Timer(60, this);
time.start();
}
@Override
public void actionPerformed(ActionEvent arg0) {
repaint();
}
// This sets the loaded image, which stops the "busy screen"
public void setImage(BufferedImage image) {
img = image;
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
// If the image is loaded.
if (img != null) {
// Then we center it on the screen as I explained above.
int scaledWidth = (int)((img.getWidth() * getHeight()/img.getHeight()));
if (scaledWidth < getWidth()) {
// Center the left and right destination x coordinates.
int leftOffset = getWidth()/2 - scaledWidth/2;
int rightOffset = getWidth()/2 + scaledWidth/2;
g.drawImage(img,
leftOffset, 0, rightOffset, getHeight(),
0, 0, img.getWidth(), img.getHeight(),
null);
}
else {
int scaledHeight = (img.getHeight()*getWidth())/img.getWidth();
int topOffset = getHeight()/2 - scaledHeight/2;
int bottomOffset = getHeight()/2 + scaledHeight/2;
g.drawImage(img,
0, topOffset, getWidth(), bottomOffset,
0, 0, img.getWidth(), img.getHeight(),
null);
}
}
// However, if the image has not been loaded yet, then
// we make a busy screen (a line that swings in a circle.
else {
int originX = getWidth()/2;
int originY = getHeight()/2;
// Trig to make the line move along a circle.
theta += Math.PI/12;
g.setColor(Color.RED);
g.drawLine(originX, originY, (int)(Math.cos(theta)*getWidth()),
(int)(Math.sin(theta)*getHeight()));
}
}
}
Happy Coding!









MultiQuote





|