Monkeys at Keyboards: The Javanomicon
© Michael James Heron
Topic: Java Programming
Level: 2
Version: delta

Electricity is actually made up of extremely tiny particles called electrons, that you cannot see with the naked eye unless you have been drinking.
Dave Barry

22 - Sound and Vision

PreviousTable of ContentsNext
Forum


Chapter Objectives

By the end of this chapter, the reader will be able to:

  • make use of sound in their Java applets.
  • make use of images in their Java applets.
  • make use of sound in their Java applications.
  • make use of images in their Java applications.


22.1

Introduction

To date, we've been using the simple GUI widgets of the Swing architecture with little concern for the aesthetics of our applications. We've looked at how to lay out our the interface using both absolute positioning (with the setBounds method) and relative positioning (using layout managers), but beyond that our applications are lacking a certain je ne sais quois in terms of their presentation.

The components we have been using are purely textual in their displays - modern interfaces are rarely so stark in their visuals. Our buttons have text labels that indicate what is to happen, but even very simple applications usually have graphical buttons that behave the same way, but provide some kind of iconographic representation of the functionality.

In this chapter, we'll be looking at how we can build on our knowledge of Java to incorporate graphics and sound into our applications, allowing us to bring to life even the dullest and most staid of programs!

22.2

Images and Applets

Returning to the subject of applets for the first part of this chapter, we will first look at how to display an image within Java's JApplet class. The procedure for doing this is somewhat different from the way we display an image using an application, so we will discuss that also in a few moments.

Java provides for us a class called Image. This class lives in the java.awt package, so any applet that is to display a graphic will need to import this.

The JApplet (and indeed, the parent Applet) classes provide a method called getImage that returns an Image object that is built up from a file stored on disk. To use the method, we pass in a URL parameter representing a directory, and another String parameter representing a filename.

Already, we've hit our first problem! When using an applet through a browser, we don't really have a way of knowing what directory it is being run from. This isn't a problem for development, but it becomes an issue with deployment - when we want to make an applet available for real life use. Providing a static URL for the first parameter is likely to end up being a very bad solution when we want to move an applet to another location.

Applets are accessed from a web-page, and Java provides the JApplet class with two methods that let us develop an adaptable way of referencing to a directory relative to the applet itself:

  • getDocumentBase, which returns a URL object that points to the location of the HTML page holding the applet.
  • getCodeBase, which returns a URL object that points to the location of the applet class file.

We can use either of these as the source of the first parameter in our getImage call. The exact method that we use depends on where the graphic file we wish to display is stored. For the purposes of this chapter, we will assume it is stored in the same directory as the HTML file for the applet, and so we will use getDocumentBase.

So, to display an image, first we need to get the Image object. Step one is declaring such an object:

Image myImage; 

Step two is actually creating the object that goes into that variable. Where we do that depends on when we want the image to be loaded. For our first example, we're going to load the image in the init method of our applet, like so:

myImage = getImage (getDocumentBase(), "persist.jpg"); 

persist.jpg is a graphic file stored in the same directory as the HTML file. In this case, it's Salvador Dali's famous Persistence of Time.

So, our applet so far looks like this:

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


public class ImageExample extends JApplet {
private Image myImage;

public void init() {
myImage = getImage (getDocumentBase(), "persist.jpg");
}


public void paint (Graphics g) {
super.paint (g);
}

}

We can compile and execute this, but our image won't display. Sadness and sorrow are surely our only companions after such a tragic letdown.

All we've done so far is create an Image object from a file on our hard-drive... we haven't actually told Java when and where to draw it.

The Graphics parameter passed to the paint method of the applet contains a useful method called drawImage, and that method allows us to place our image on the screen.

The drawImage method takes six parameters:

  1. The Image object that is to be drawn.
  2. The x co-ordinate of the top left hand corner of where we want the image to be drawn.
  3. The y co-ordinate of the top left hand corner of where we want the image to be drawn.
  4. The length that we want the image to be drawn with.
  5. The height that we want the image to be drawn with.
  6. An object to be informed when the image is finished drawing.

The sixth parameter is too complex for us to go into in this section, so we'll ignore it and always use the value this as we have in previous chapters. We'll come back to this parameter at the end of the chapter.

The 2nd, 3rd, 4th and 5th parameters should look familiar - they're exactly the same ones as used in a setBounds call. We pass these four parameters to define a bounding rectangle, and Java attempts to draw the image within this rectangle to the best of its ability. Java will automatically stretch or shrink the image to fit the rectangle we give it.

We put a call to drawImage in our paint method to display our image:

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


public class ImageExample extends JApplet {
private Image myImage;

public void init() {
myImage = getImage (getDocumentBase(), "persist.jpg");
}


public void paint (Graphics g) {
super.paint (g);
g.drawImage (myImage, 20, 20, 300, 300, this);
}

}

Now when we compile and execute, our applet will display the image we have given it in all its Technicolor glory!

Persistance of Time Applet
Fig 22.1: Persistance of Time Applet

Shucks, that sure looks purty! And as you can see, it's very easy to do. We have three steps:

  1. Declare an Image object with appropriate scope.
  2. Put something into the Image object using the getImage method.
  3. Draw the image on the screen using the drawImage method of the Graphics object passed into paint.

This is how we can place traditional images on our applet - but when developing graphical representations for GUI components (like a JButton), we have to do a little something extra. We'll come to that later in the chapter.

Run this applet

Click here to see the applet ImageExample working.


22.3

Sounds in Applets

We can setup a sound to be played in a very similar way in a Java applet. We don't use the Image class (obviously) - instead we use a class called AudioClip. So step one is to declare an instance of that class with appropriate scope:

AudioClip mySound; 

Then we use the getAudioClip method of JApplet to put something into the object. This works exactly the same way as the getImage method we saw above, even down to the parameters:

mySound = getAudioClip (getDocumentBase(), "some_sound.wav"); 

When we wish to play the sound, we call a method on our sound object:

Table of Sound Methods
Fig 22.2: Table of Sound Methods

We can call this method anywhere in the applet - it doesn't have to be when the applet is painted. However, the ease with which sounds can be played comes with a price, and that price is that the AudioClip object is somewhat limited. It doesn't allow MP3s to be played, much less any more recent compressed audio format. Only simple, uncompressed WAV and Midi files are supported, which greatly limits what you can do. You are not going to be able to write that MP3 jukebox program you've always wanted using the AudioClip class.

Let's look at an example of an applet that plays a sound (or three). In this case, it's an applet that will play a different laugh track depending on which button is pressed. We start off by creating our interface (which consists of three buttons):


public class Sound extends JApplet implements ActionListener {
JButton myButton1, myButton2, myButton3;

public void init() {
Container c= getContentPane();
myButton1 = new JButton ("Laugh One");
add (myButton1, BorderLayout.NORTH);
myButton1.addActionListener (this);
myButton2 = new JButton ("Laugh Two");
add (myButton2, BorderLayout.CENTER);
myButton2.addActionListener (this);
myButton3 = new JButton ("Laugh Three");
add (myButton3, BorderLayout.SOUTH);
myButton3.addActionListener (this);
}


public void actionPerformed (ActionEvent e) {
}

}

Then we need to create our AudioClip objects. We'll need three of these, one for each laugh sound file. We then put something into these objects declarations by using the getAudioClip method:


public class Sound extends JApplet implements ActionListener {
AudioClip laugh1, laugh2, laugh3;
JButton myButton1, myButton2, myButton3;

public void init() {
Container c= getContentPane();
myButton1 = new JButton ("Laugh One");
add (myButton1, BorderLayout.NORTH);
myButton1.addActionListener (this);
myButton2 = new JButton ("Laugh Two");
add (myButton2, BorderLayout.CENTER);
myButton1.addActionListener (this);
myButton3 = new JButton ("Laugh Three");
add (myButton3, BorderLayout.SOUTH);
myButton2.addActionListener (this);
laugh1 = getAudioClip (getDocumentBase(), "laugh1.wav");
laugh2 = getAudioClip (getDocumentBase(), "laugh1.wav");
laugh3 = getAudioClip (getDocumentBase(), "laugh2.wav");
-BOLD }


public void actionPerformed (ActionEvent e) {
}

}

And then we add the logic for playing the sound. The sound we play is dependant on the button we press. The code for this goes into the actionPerformed method:


public void actionPerformed (ActionEvent e) {
if (e.getSource() == myButton1) {
laugh1.play();
}

if (e.getSource() == myButton2) {
laugh2.play();
}

if (e.getSource() == myButton3) {
laugh3.play();
}

}

When we run this applet, we'll get different laugh sounds played whenever we click on a button. Excellent, and something I would very much like to have as a personal electronic device. I've often felt that my life needs more canned laughter, and I'm sure if you spend some time thinking about it, you'll feel the same way about your own lives.

Run this applet

Click here to see the applet Sound working.


22.4

Images in Applications

Applets have a simplified syntax for accessing Image and AudioClip files stored on a hard-drive - this is a consequence of the sandbox model for an applet. Unrestricted access to a hard-drive is forbidden, and so an extra layer of abstraction is provided for applets to ensure they are not restricted as far as vital common functionality is concerned.

Applications have no such restrictions, and therefore no such simplified syntax for displaying images. We can't simply call getImage in an application's constructor method, because an application has no getImage method. Alas!

However, Java does provide a powerful class called Toolkit, which contains many of the utility methods used by JApplets and Applets. One of the utility methods that it provides is a method called getImage. Huzzah!

Is there a catch? Of course there's a catch - the catch is that this getImage method doesn't work quite the same way as an Applet getImage method. The inconsistency between the two can be quite confusing - you must remember what kind of program you are working on and apply the appropriate framework code.

Of course, there is a consequence that it becomes more difficult to convert an Applet into an Application when it is using graphics, but that's nothing that ten to twelve hours of a frustrating search and replace session won't put right!

In order to make use of the Toolkit class, we first need a reference to it. We don't create an instance of this in the same way we do most classes - instead, we call a static method on the Toolkit class itself... this method is called getDefaultToolkit.

Step one, we need an object reference:

Toolkit myTools; 

Then we get an instance of the toolkit:

myTools = Toolkit.getDefaultToolkit(); 

And then we're good to go! The getImage method of the Toolkit has a simpler syntax - all we pass is a filename that is either relative or absolute. Usually we'll use a relative filename:

Image myImage; 
myImage = myTools.getImage ("persist.jpg");

The JFrame class has a method called paint that works in exactly the same way as the paint method we've used above for an applet, so from this point on it's exactly the same procedure to display the image on the screen... calling drawImage from the Graphics object:


public class ImageExample extends JFrame {
private Image myImage;

public ImageExample() {
Toolkit myTools = Toolkit.getDefaultToolkit();
myImage = myTools.getImage ("persist.jpg");
}


public void paint (Graphics g) {
super.paint (g);
g.drawImage (myImage, 20, 20, 350, 300, this);
}


public static void main (String args[]) {
ImageExample mainWindow = new ImageExample();
mainWindow.setSize (400, 350);
mainWindow.setTitle ("Show me the image!");
mainWindow.setVisible (true);
}

}

22.5

Sounds in Applications

We have a similar problem with sound in that we have no access to a getAudioClip method in an application. Using the toolkit won't help either, because the method is not there either.

The Applet class however has a method we can use, and that method is newAudioClip. This returns an AudioClip object in the same way that getAudioClip does.

What do you mean 'where's the catch?'. Are you already so cynical that you assume that nothing can be as easy as it looks? Do you really believe that you have to go through excruciatingly obtuse routines to get even the simplest results? That's sad, and it really disappoints me that... oh, okay. I'll tell you the catch.

The catch is that the parameter to this method is an object of type URL... so we can't simply pass in a filename. Curses.

How do we make a URL connection to a local file? Simple, we use the file: protocol of HTTP to define a connection to a local resource. We can use this as a parameter to an object of type URL (which we saw in the chapter 20):

URL myURL = new URL ("file:c:/bing.txt"); 

Ah, but the Goddess of Portability screams in fear at such callous disregard for her needs! Defining full paths in such a way is Bad Voodoo as far as maintainability and portability are concerned, and so this is a less than perfect solution.

What on earth can we do to resolve our woes?

In previous chapters, we have briefly mentioned the existence of a class called System. This class has a very useful method called getProperty that can be used to query some of the underlying constants of the host operating system. Applications have very easy access to this method... applets have a somewhat restricted set of options.

One of the available properties is called user.dir, and that property contains the working directory for an application. We can use this in much the same way as getDocumentBase to ensure that when we move an application around a directory we don't need to make significant changes to the underlying code.

We get the working directory like so:

String dir = System.getProperty ("user.dir"); 

The returned directory does not include a trailing slash, so you will need to add this yourself.

So, to create a reference to a local sound stored on your hard drive:

String baseDir = System.getProperty ("user.dir"); 
String filename = "laugh.wav";
String fullPath = "file:" + baseDir + "\\" + filename;
URL myURL = new URL (fullPath);

Zounds! That's an awful lot of code to achieve what was so simple a goal with an Applet.

Once we've constructed this URL, we can use it as the parameter to newAudioClip:

AudioClip mySound; 
mySound = Applet.newAudioClip (myURL);

And that's us finished... from this point on, it's plain sailing and works in exactly the same way as playing sounds within an applet.

For an application version of our Canned Laughter applet above, we'd use the following code:

import java.awt.*; 
import java.applet.*;
import java.awt.event.*;
import javax.swing.*;
import java.net.*;


public class Sound extends JFrame implements ActionListener {
AudioClip laugh1, laugh2, laugh3;
JButton myButton1, myButton2, myButton3;

public Sound() {
Container c= getContentPane();
String baseDir = System.getProperty ("user.dir");
try {
laugh1 = Applet.newAudioClip (new URL ("file:" + baseDir + "\\" + "laugh.wav"));
laugh2 = Applet.newAudioClip (new URL ("file:" + baseDir + "\\" + "laugh1.wav"));
laugh3 = Applet.newAudioClip (new URL ("file:" + baseDir + "\\" + "laugh2.wav"));
}

catch (MalformedURLException ex) {
JOptionPane.showMessageDialog (null, "Bad URL == Bad Boy!");
}

myButton1 = new JButton ("Laugh One");
add (myButton1, BorderLayout.NORTH);
myButton1.addActionListener (this);
myButton2 = new JButton ("Laugh Two");
add (myButton2, BorderLayout.CENTER);
myButton1.addActionListener (this);
myButton3 = new JButton ("Laugh Three");
add (myButton3, BorderLayout.SOUTH);
myButton2.addActionListener (this);
}


public void actionPerformed (ActionEvent e) {
if (e.getSource() == myButton1) {
laugh1.play();
}

if (e.getSource() == myButton2) {
laugh1.play();
}

if (e.getSource() == myButton3) {
laugh2.play();
}

}


public static void main (String args[]) {
Sound mainWindow = new Sound();
mainWindow.setSize (400, 400);
mainWindow.setTitle ("Sounds!");
mainWindow.setVisible (true);
}

}

As you can see, that's nowhere near as developer-friendly as with an applet. Alas, that is our cross to bear as aspiring Java developers.

22.6

Time to Come Clean

We've been using the paint method quite a bit in this chapter - and as you'll undoubtedly recall, we spent quite a bit of time in chapter one looking at what we could do with it. Unfortunately, it is bad practise to use the paint method for... well, all of the things we've been using it for.

It's not a trivial process to setup a proper Swing application with graphics built into a user interface - the drawImage method is not very polite and it will simply obscure any components we have in our interface if they overlap. For example, let's add a button to our Persistance Of Time application:

A Glitch With Graphics
Fig 22.3: A Glitch With Graphics

Look at the north portion of the application... you can clearly see the button, but the image overlaps it. It'll still work... when we click on it it will draw the buton over the image... at least until the paint method gets called again.

Ideally we'd be able to treat images in the same way that we treat GUI components... we should be able to place them according to a layout manager. Thankfully that's exactly what we can do, but we need to spend a little more time setting up the program.

Properly we should handle images in their own separate class - one that extends the JPanel class that we spoke about in chapter 9. In this class, we define a method called paintComponent, and in there we handle all the graphics functionality.

paintComponent is a standard method that is called automatically in the lifetime of an applet or application - it's like the base paint method... you don't need to call it at any point, Java does it for you.

paintComponent gets called at the same times as paint gets called - in fact, it works exactly the same way as the paint method, even down to the need to call the parent paintComponent method. It just has a different name.

So let's look at our application above rewritten to use its own separate panel for the image. First we need a class that extends JPanel... that's simple enough by this point:

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


public class PersistPanel extends JPanel {
}

And then we need to create the Image object:

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


public class PersistPanel extends JPanel {
Image myImage;

public PersistPanel() {
Toolkit myTools = Toolkit.getDefaultToolkit();
myImage = myTools.getImage ("persist.jpg");
}

}

Then all we need to do is provide the paintComponent method. Our parameters to the drawImage method will be slightly different - we have no idea, in advance, in what kind of context our panel may be used. We don't know the size of the application (well, we do for this example... but we may want to use it in other applications in the future), and we don't know the orientation or location the image will be placed.

We deal with this ambiguity by letting the panel tell us what we need to know. We want to draw the image, and we want it to work like a standard component... so let's draw it the full size of the panel.

We start drawing from 0,0 (the top left corner of the panel), and we draw it the length and height of the panel... we get these values from the methods getWidth() and getHeight():

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


public class PersistPanel extends JPanel {
Image myImage;

public PersistPanel() {
Toolkit myTools = Toolkit.getDefaultToolkit();
myImage = myTools.getImage ("persist.jpg");
}


public void paintComponent (Graphics g) {
super.paintComponent (g);
g.drawImage (myImage, 0, 0, getWidth(), getHeight(), this);
}

}

And now we can simply place it on the application as if it were a standard JPanel:

import java.awt.*; 
import java.applet.*;
import javax.swing.*;


public class PaintComponentExample extends JFrame {
JButton myButton;
PersistPanel persistanceOfTime;
public PaintComponentExample() {
myButton = new JButton ("Press me!");
persistanceOfTime = new PersistPanel();
add (persistanceOfTime, BorderLayout.CENTER);
add (myButton, BorderLayout.NORTH);
}


public static void main (String args[]) {
PaintComponentExample mainWindow = newPaintComponentExample();
mainWindow.setSize (400, 350);
mainWindow.setTitle ("Show me the image!");
mainWindow.setVisible (true);
}

}

Now we can display an application that has an image that doesn't draw over any of our components... and what's more, it'll resize as the application resizes! How cool is that?

Really, we should now be using separate panels for all of our graphical requirements... it takes a little bit of extra time (and can complicate coupling issues if the graphics are dependant on the state of the rest of the application). We'll see a further example of this idea in a future case study.

Java tip

We won't know the size of the panel before it is placed on the application, but we know that paintComponent will be called whenever the paint method of the application is called, and we know it gets called when an application is created and when the window is resized. We can then get the current size of the panel through the getHeight() and getWidth() methods of JPanel and base any internal sizing of images on the current relative size of the panel.


22.7

Swing Graphical Components

Most Swing components allow a graphic to be used in place of text. This is an immense improvement over the more limited AWT set of libraries. The graphical display of a swing component is handled through a new class called ImageIcon.

This class can be used to create an iconic version of any Image file that you've previously created. All you need to do is create a new instance of ImageIcon using the desired Image file as a parameter to its constructor. Then, supported Swing components can use their setIcon method to use the icon for display

Image myImage; 
ImageIcon myIcon;
void init() {
myImage = getImage (getDocumentBase(), "filename.jpg");
myButton = new JButton ("Press me!");
add (myButton, BorderLayout.NORTH);
myIcon = new ImageIcon (myImage);
myButton.setIcon (myIcon);
}

A JButton in particular is well suited for this kind of display... in fact, it has a range of extra methods associated with this kind of interface design:

Table of JButton ImageIcon methods
Fig 22.4: Table of JButton ImageIcon methods

These don't all need to be set - only the ones you are interested in using.

We can even specify an ImageIcon as being the preferred mode of display when we create an instance of a JButton without needing to call setIcon at all:

myButton = new JButton (myIcon);

This will create a button with an icon, but no text - as I'm sure you're aware, this is a very common style throughout applications of all forms.

Let's look at a simple example of this - we'll use an image file of the classic Ying-Yang symbol for our default icon, and a version with inverted colours for the rollover icon:

import java.awt.*; 
import java.applet.*;
import javax.swing.*;
import java.awt.event.*;


public class ImageIconExample extends JApplet {
JButton myButton;

public void init() {
Image myImage, myImage2;
ImageIcon myIcon;
ImageIcon myIcon2;
myImage = getImage (getDocumentBase(), "ying.jpg");
myImage = getImage (getDocumentBase(), "yang.jpg");
myIcon = new ImageIcon (myImage);
myIcon2 = new ImageIcon (myImage2);
myButton = new JButton (myIcon);
myButton.setRolloverIcon (myIcon2);
add (myButton, BorderLayout.NORTH);
}

}

When we compile and execute this applet, we have an icon based button that changes its icon dependant on whether or not the mouse pointer is hovered over it:

Rollover ImageIcons
Fig 22.5: Rollover ImageIcons

Run this applet

Click here to see the applet ImageIconExample working.


You can even change the icon being displayed as an application is running... the component will automatically resize to support the new icon. As you can image, implementing things like slideshows and such are very easy using this kind of structure.

You can even set icons to be animated gifs! Of course, anyone who does this for any reason other than to illustrate the diverse functionality of the ImageIcon class deserves to be beaten to death with wild dogs for their assault against useable interface design.

Sometimes you may find that your ImageIcons don't actually scale properly - if the image is too big for the component, it will only draw what it can. You can solve this through the use of the Image object's getScaledInstance method - by using this method you can ensure the image will be a certain height and width. The method takes three parameters - one is the length, one is the height, and the third is which algorithm to use for scaling - you use one of the constant values in the image class for this:

myImage = myImage.getScaledInstance (100, 100, Image.SCALE_FAST); 

It takes a lot of work and planning to set up a good interface using ImageIcons - icons should give an indication of what they do as well as look good. Interested readers are directed towards any of the fine reference books on the subject of user interface design for more details.

Java tip

Graphical buttons look great, but don't forget that people don't know what's going on in your mind. Don't assume that because you put a picture of a tree on a button that people will think 'Ah-ha - this button is the exit button, because I should make like a tree and leave!'. Many elements of functionality have standard defined icons (like a pair of scissors for 'cut') - don't deviate from these without good reason, and put tooltips on the button to provide a hint for those who need it.


22.8

Animation

Finally in this chapter we're going to look at some simple animation in Java. You don't need anything approaching art ability to do this - we're not attempting to overthrow Disney, after all.

The vehicle we're going to use to implement animation in our Java programs is a method called repaint. The repaint method is responsible for clearing everything that has previously been painted on the screen, and then calling the paint method... it's a housekeeper method. In the course of the paint method being called, it will also automatically call paintComponent on all of the panels.

If we call the repaint method from within the paint method, what we get is a very neat loop system that we can tap into to move images across the screen. Paint calls repaint which calls paint which calls... etc, etc, et

We're going to draw a ball that bounces off of the sides of our applet/application. The technique for doing this is very simple. The Graphics class has a method called drawOval that takes the typical setBounds set of parameters:

g.drawOval (x, y, len, ht); 

The size of a ball is going to remain constant, so len and ht will remain unchanged throughout. All we're going to change is where we draw the ball... so by modifying x and y, we can choose a new top-left point for the ball.

If we call repaint after drawing this ball, the repaint method will clear the ball off of the screen and call paint again - if we move x and y a little before drawing the ball, we get the illusion of movement in exactly the same way traditional animation works. I hope I haven't burst any bubbles there, but Mickey Mouse isn't really moving when you watch him. It's an illusion.

So, x and y are going to be variable, and so we need variables to hold them. We also need variables that hold the value of by how much we change x and y each time around the loop. Remember that we start counting from the top left in an applet as opposed to the bottom left as with a typical math graph:

Applet Co-ordinates
Fig 22.6: Applet Co-ordinates

As the value of x increases, the ball will move right across the screen. As the value y increases, it will move down the screen. If x decreases, the ball will move left, and if y decreases the ball will move upwards.

So we need an x and y variable:

int x, y; 

And we need variables that hold how by how much we are going to modify x and y:

int xdir = 3; 
int ydir = 3;

At each stage in paint, we add xdir to x and ydir to y, then draw the ball:


public void paintComponent (Graphics g) {
int len = 100;
int ht = 100;
x = x + xdir;
y = y + ydir;
g.fillOval (x, y, len, ht);
repaint();
}

That's all it takes for some very simple animation - but the ball will move off the side of the screen and never be seen again. We also need to have some code for implementing the 'bouncing' part.

When x reaches some arbitrary limit (let's say 300), we want it to start moving left. If we're modifying x by a positive number, we need to start modifying it by a negative number:

xdir = xdir * -1; 

And the same with y:

ydir = ydir * -1; 

We also want to do the same thing when x or y reach 0. So:


public void paintComponent (Graphics g) {
int len = 100;
int ht = 100;
if (x > 300 || x < 0) {
xdir = xdir * -1;
}

if (y > 300 || y < 0) {
ydir = ydir * -1;
}

x = x + xdir;
y = y + ydir;
g.fillOval (x, y, len, ht);
repaint();
}

We're almost there, but it doesn't quite work yet. The reason for this is that we are checking the boundary condition on the top left corner of the ball, not the actual point of contact:

Boundary Condition Problem
Fig 22.7: Boundary Condition Problem

We need to take the length and height of the ball into account when we check against the right hand side of the applet and when we check against the bottom. The actual point of contact for the right hand side is the width of the panel minus the length of the ball, and the actual point of contact for the button is the height of the panel minus the height of the ball:


public void paintComponent (Graphics g) {
int len = 100;
int ht = 100;
if (x >= (getWidth() - 100) || x <= 0) {
xdir = xdir * -1;
}

if (y >= (getHeight() - 100) || y <= 0) {
ydir = ydir * -1;
}

x = x + xdir;
y = y + ydir;
g.fillOval (x, y, len, ht);
repaint();
}

We can put all of this into an extended JPanel, and provide ourselves with a (fairly useless) bouncing ball component:

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


public class BallPanel extends JPanel {
int x, y, xdir, ydir;

public BallPanel() {
// We start drawing the image at 100, 100.
x = 100;
y = 100;
// We're going to change x by 3 and y by 2 every time we
// go through the loop.
xdir = 3;
ydir = 2;
}


public void paintComponent (Graphics g) {
super.paintComponent (g);
g.fillOval (x, y, 100, 100);
if (x >= (getWidth() - 100) || x <= 0) {
xdir = xdir * -1;
}

if (y >= (getHeight() - 100) || y <= 0) {
ydir = ydir * -1;
}

x += xdir;
y += ydir;
repaint();
}

}

We can implement animation of images in exactly the same way, simply by replacing the g.fillOval method call with a call to drawImage. Consider the same structure for a panel that allows the user to set an image to animate:

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


public class ImageAnimPanel extends JPanel {
int x, y, xdir, ydir;
Image myImage;

public ImageAnimPanel (String image) {
Toolkit myTool = Toolkit.getDefaultToolkit();
x = 100;
y = 100;
xdir = 3;
ydir = 2;
myImage = myTool.getImage (image);
}


public void paintComponent (Graphics g) {
super.paintComponent (g);
g.drawImage (myImage, x, y, 100, 100, this);
if (x >= (getWidth() - 100) || x <= 0) {
xdir = xdir * -1;
}

if (y >= (getHeight() - 100) || y <= 0) {
ydir = ydir * -1;
}

x += xdir;
y += ydir;
repaint();
}

}

It may take a little while for the image to load, but the effect will be a bouncing image file instead of a simple bouncing ball.

Java tip

Simple animation can be accomplished using these simple rules - for more complex animations, it makes more sense to create an array of externally created (such as with Photoshop) image files and simply step through to the next of these each time paintComponent is called - in this way, very detailed animation can be accomplished with very little code.


22.9

Slow Image Performance

One of the big complaints regarding images in Java relates to their performance... they are often very slow to render and display. When we call getImage, we're not actually loading the image from the disk - we're just creating the Image object for when we do. We never actually load the image until we call drawImage... if we have a lot of drawImage calls to a lot of images, this can cause severe performance delays.

We can force Java to load an image before it is displayed by using the prepareImage method which is built into all Java components. We pass this two parameters - one is the image to prepare, the other is an object to be informed when the preparation has been completed - again, we just pass this for the second parameter:

Image myImage; 
myImage = myToolkit.getImage ("Bing.gif");
prepareImage (myImage, this);

The prepareImage method spawns off another thread (more on this later) to deal with the image loading - the net result is that although there is still a delay whilst the images are being loaded, graphics that are accessed later will be instantly drawn on the screen since they've already been loaded.

We may not actually want to do anything with our programs until our images have finished loading - it looks quite shabby to have an application that draws everything slowly and seemingly randomly. Real programs don't do that... they wait until everything is loaded before they display anything. Sometimes this is done via a splash screen (which we are very much capable of doing at this point), but sometimes it's just done with a simple 'Loading' string.

The key to implementing this in a Java program relates to the sixth parameter of the drawImage method (or the second parameter of the prepareImage method). As mentioned above, this is an object that should be notified when the image is ready to be drawn. We've just ignored it up until this point.

The object referenced requires a method called imageUpdate (this is provided in one of the parent classes of JFrame/JApplet). We can over-ride the method to provide our own functionality to deal with images that are still loading.

The method has six parameters:

  1. The image being references
  2. An integer representing the current state of the image loading
  3. The x co-ordinate
  4. The y co-ordinate
  5. The length
  6. The height

We provide our own implementation of this method in our code to handle our own required functionality.

The method returns a boolean value - we return false if we don't want to track the loading of the image any more (usually done when we're finished loading it). We return false if we want further updates on the progress of the loading.

The second parameter relates to how much of the image has been loaded. There is a special variable available to us in our applications and applets - ALLBITS. If the second parameter is equal to this, then the image has finished loading. Let's look at how to change our ImageAnimPanel so that we can tell people that our image is still loading:

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


public class ImageAnimPanel extends JPanel {
int x, y, xdir, ydir;
Image myImage;
boolean loaded = false;
public ImageAnimPanel (String image) {
Toolkit myTool = Toolkit.getDefaultToolkit();
x = 100;
y = 100;
xdir = 3;
ydir = 2;
myImage = myTool.getImage (image);
prepareImage (myImage, this);
}


public void paintComponent (Graphics g) {
super.paintComponent (g);
if (loaded) {
g.drawImage (myImage, x, y, 100, 100, this);
}

else {
g.drawString ("Loading Image", x, y);
return;
}

if (x >= (getWidth() - 100) || x <= 0) {
xdir = xdir * -1;
}

if (y >= (getHeight() - 100) || y <= 0) {
ydir = ydir * -1;
}

x += xdir;
y += ydir;
repaint();
}

public boolean imageUpdate (Image img, int status, int x, int y, int
len, int ht) {
if (status == ALLBITS) {
loaded = true;
repaint();
return false;
}

return true;
}

}

Our call to prepareImage is what starts the imageUpdate invocations - Java will look for an imageUpdate method in the object passed as the second parameter. In this case, it finds our overloaded version. We check and see if the status variable is equal to the ALLBITS variable. If it is, we set the boolean loaded to true, and call repaint (so that paintComponent is called once again. We then return false to indicate that we have no more need for tracking the loading of images.

If the status doesn't equal ALLBITS, then we return true - as long as we return true, imageUpdate will be called periodically as the image is loaded.

In our paintComponent method, we simply check to see if the loaded variable is true. If it is, we draw the image. Otherwise we draw a string indicating that we are still loading the files.

This doesn't actually speed up the loading of images, but it does provide the potential for implementing splash screens that can take away some of the perception of passing time.

Java tip

If your application or applet is Image Heavy, you will need to implement a loading system via a splash-screen, or load images in the background whilst other processing is going on. As of yet, there is no equivalent functionality outside of the specialised Java APIs (which are not covered in this book) for dealing with Java programs that are similarly Audio Heavy.


22.10

Graphics2D

The last thing we'll look at in this chapter is Graphics2D, which is a specialisation of the standard Graphics object we have available to us within our applets and applications. Graphics2D offers a range of methods beyond those offered by the standard Graphics object.

To get access to a Graphics2D object, we need to make use of polymorphism (more on this later, of course) and cast the Graphics object we get from paint (or paintComponent) into a Graphics 2D object:

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


public class Graphics2DPanel extends JPanel {

public void paintComponent (Graphics g) {
Graphics2D g2;
super.paintComponent (g);
g2 = (Graphics2D) g;
}

}

For backwards compatability with AWT, the paint and paintComponent methods upcast any Graphics2D objects they are passed by the standard framework into vanilla Graphics objects. Rest assured though, they are actually 2D objects internally.

Once we've got our 2D object, we can start playing about with some of its neat methods. All of the standard Graphics methods are there - we just get some extra special additions.

For example, there's a rotate method - pass that a number of radians (as a double), and it rotates the drawing area. This can make it more difficult to find co-ordinates, but it lets you do some neat things. For example:

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


public class Graphics2DPanel extends JPanel {

public void paintComponent (Graphics g) {
Graphics2D g2;
super.paintComponent (g);
g2 = (Graphics2D) g;
g2.rotate (45.0);
g2.drawString ("Hello World!", 100, 20);
}

}

Executing this in an appropriate context gives us some text written a 45 radian angle:

Rotated Text
Fig 22.8: Rotated Text

Or how about:

Rotated Image
Fig 22.9: Rotated Image

Or how about a rotating image? It's easily done. The only difficulty is working out where the x and y co-ordinates should be... normally we start counting from the top left corner, but if we rotate the drawing context it becomes difficult to work out where things should be in relation.

The simplest solution is to change where we begin drawing from... we can do this using the transform method... this lets us change the origin of the drawing context:

g1.translate (this.getWidth() /2, this.getHeight() /2); 

This causes Java to draw, from that point on, everything from a point in the middle of the panel. So for our spinning image:

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


public class Graphics2DPanel extends JPanel {
Image img;
int radians;

public Graphics2DPanel() {
Toolkit myTools = Toolkit.getDefaultToolkit();
img = myTools.getImage ("persist.jpg");
radians = 0;
}


public void paintComponent (Graphics g) {
Graphics2D g2;
super.paintComponent (g);
g2 = (Graphics2D) g;
radians += 1;
if (radians > 100) {
radians = 1;
}

g1.translate (this.getWidth() /2, this.getHeight() /2);
g1.rotate (radians);
g1.drawImage (img, 0, 0, 100, 100, this);
repaint();
}

}

Java tip

If you are going to be changing the drawing angle using Graphics 2D, it is vital that you translate the center of the drawing area - otherwise you'll find it very difficult to work out where things are going to be drawn.


Graphics2D objects are somewhat more complex than the standard Graphics objects - but that extra complexity allows for more expression in terms of what can actually be drawn. The full extent of what we can do is sadly beyond the scope of this chapter, but we'll look at two new elements - the Pen and a Fill. By combining these we can add definition and fill effects to any shape we wish to draw.

There are some new classes we should look at before we start trying these things out - they're located in java.awt.geom. The first we'll look at is called RoundRectangle2D.

The syntax for setting this up is kind of obscure... there are two direct subclasses of RoundRectangle2D... one is RoundRectangle2D.Double (yes, with a period in the middle) and the other is RoundRectangle2D.Float. They indicate what kind of parameters the constructor will receive. The Double version takes the parameters in doubles, and the Float version takes the parameters in... you guessed it, floats.

The constructor method for both takes six parameters - the first four are the standard x, y, length and height... the fifth is the width of the rounding at the corners, and the sixth is the height of the rounding at the corners.

We then use one of the Graphics2D drawing methods to put the shape on the screen - we can draw, which does the outline, or fill which does a filled shape:

import javax.swing.*; 
import java.awt.*;
import java.awt.geom.*;


public class Graphics2DPanel extends JPanel {
Image img;

public void paintComponent (Graphics g) {
Graphics2D g2;
RoundRectangle2D.Double roundRect;
super.paintComponent (g);
g2 = (Graphics2D) g;
roundRect = new RoundRectangle2D.Double (10.0, 10.0, 100.0, 100.0, 9.0,
9.0);
g1.fill (roundRect);
}

}

This code gives us the following graphic:

A Rounded Rectangle
Fig 22.10: A Rounded Rectangle

That probably seems like a lot of work for a rounded rectangle, but now that we have the shape we can start applying new pen and fill effects.

For example, it might be nice to have the shape filled with a gradient colour (like the kind you get along the top of your application). We can do this by creating an instance of GradientPaint and passing it to setPaint of the Graphics2D object.

The GradientPaint constructor takes six parameters. The first two are the x and y co-ordinate of the start point of the gradient. The third is the colour to use for the start gradient. The fourth and fifth are the x and y co-ordinates of the end point of the gradient, and the sixth is the colour to gradually move towards:

import javax.swing.*; 
import java.awt.*;
import java.awt.geom.*;


public class Graphics2DPanel extends JPanel {

public void paintComponent (Graphics g) {
Graphics2D g2;
GradientPaint gp;
RoundRectangle2D.Double roundRect;
super.paintComponent (g);
g2 = (Graphics2D) g;
roundRect = new RoundRectangle2D.Double (10.0, 10.0, 100.0, 100.0, 50,
50);
gp = new GradientPaint (0, 0, Color.GREEN, 100, 100, Color.BLUE);
g1.setPaint (gp);
g1.fill (roundRect);
}

}

This code gives us the following graphic:

A Colourful Rounded Rectangle
Fig 22.11: A Colourful Rounded Rectangle

There is much more that can be done with the Graphics2D object - the interested reader is directed towards the further reading section of this chapter.

22.11

Conclusion

There is much more to the subject of Graphics and Sound in Java than we have time to cover in this particular text. The code we have discussed in this chapter should however give you enough information to start including graphical components in your applications.

Java provides a richer set of methods for dealing with graphics than we have discussed... in particular the Graphics2D class includes a wide range of drawing methods that are well worth investigating. Have a look at the Java documentation for more details.

Further Reading

The following table details further reading on the topic in this chapter, and also any external resources that you may find useful.

ResourceDescription
Example Programs from this chapterThis is a zip file of all the programs shown in this chapter.

PreviousTable of ContentsNext

© 2004-2006 Michael James Heron