![]() | Monkeys at Keyboards: The Javanomicon © Michael James Heron | ||||
| Topic: Java Programming Level: 2 Version: delta | |||||
22 - Sound and Vision | |||||
| Previous | Table of Contents | Next |
| Forum |
| Chapter Objectives |
By the end of this chapter, the reader will be able to:
|
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!
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:
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:
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:
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:
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:
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:
Now when we compile and execute, our applet will display the image we have given it in all its Technicolor glory!
Shucks, that sure looks purty! And as you can see, it's very easy to do. We have three steps:
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.
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:
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:
When we wish to play the sound, we call a method on our sound object:
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):
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:
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:
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.
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:
Then we get an instance of the toolkit:
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:
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:
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):
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:
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:
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:
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:
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.
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:
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:
And then we need to create the Image object:
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():
And now we can simply place it on the application as if it were a standard JPanel:
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.
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
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:
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:
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:
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:
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.
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:
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:
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:
And we need variables that hold how by how much we are going to modify x and y:
At each stage in paint, we add xdir to x and ydir to y, then draw the ball:
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:
And the same with y:
We also want to do the same thing when x or y reach 0. So:
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:
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:
We can put all of this into an extended JPanel, and provide ourselves with a (fairly useless) bouncing ball component:
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:
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.
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:
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:
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:
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.
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:
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:
Executing this in an appropriate context gives us some text written a 45 radian angle:
Or how about:
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:
This causes Java to draw, from that point on, everything from a point in the middle of the panel. So for our spinning image:
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:
This code gives us the following graphic:
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:
This code gives us the following graphic:
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.
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 ReadingThe following table details further reading on the topic in this chapter, and also any external resources that you may find useful.
|
| Previous | Table of Contents | Next |
© 2004-2006 Michael James Heron