Assignment 11 - A Few PixTrix
In assignment 10, you learned how to load and display images. For Assignment 11, you will duplicate the PixTrix applet which you can see running below. (Of course, you should add an image of your own choice).
PixTrix
To run PixTrix, add a PARAM tag with NAME="image" and VALUE set to the name of a GIF or JPG image. When PixTrix first starts, it displays your image in the center of the applet at a size of 25 x 25 pixels.
After it draws your image, PixTrix also draws four "sizing handles," one in each corner. If you click on one of the sizing handles, you can resize the picture by dragging. If you click and drag in the middle of your image, you can move it around your canvas.
While this might seem like a fairly rudimentary exercise, the techniques you learn actually have wide application; you'd use the same sort of code in a card-playing game, for example.
After your finished moving my picture around, pick up your JDK, a GIF or two, and get ready to write some code.
To complete the homework, you'll have to understand how to:
- Load an Image in an applet.
- Use the MediaTracker class to wait for an Image to load.
- How to draw an Image using the Graphics drawImage() method.
- How to move and resize your Image when you repaint it.
In addition, you'll find information on using Images in your text.
Loading Images
Java programs automatically know how to deal with images stored in the GIF format and the JPEG format. The Applet class has a built-in method, getImage(), that will load the image from a file. The file, however, must be stored on the same server as your applet--the applet cannot load the file from your local machine.
Some browsers--IE4, for instance--will make an exception to this rule if the applet is opened as a file. The latest version of Navigator, however, will not. That means, you'll have to test your program using appletviewer, or by loading it onto the server.
The Applet getImage() method takes two arguments: the first argument is the URL where the file is located; you'll almost always simply pass getDocumentBase() or getCodeBase() for this argument. The getDocumentBase() method returns the URL of the directory that contains your HTML file, while the getCodeBase() method returns the URL of the directory that contains the Java applet itself.
The second argument to getImage() is a relative path to the file containing the image. Often, this will just be the name of the file. You can also pass a directory name like this:
Image img = getImage(getDocumentBase(),
"images/redball.gif");
When you call the getImage() method, you'll store the result in an Image object, like img in the line above.
Synchronous and Asynchronous Methods
The getImage() method is an asynchronous method. That means it doesn't wait until it has retreived your Image before it returns; instead, it starts working in the "background", and lets the rest of your program continue. This makes your program much more responsive, but it also creates some potential problems.
You may want to find out how big the image is, for instance. The Image class has a pair of useful methods that allow you to do just that--getWidth() and getHeight()--but they don't work until the entire image has arrived from the server. A similar circumstance occurs when you try to display your image in the paint() method, using the GraphicsdrawImage() method. Because paint() may be called before getImage() is really finished retrieving the image from your server, the call to drawImage() may fail--your Image object may still be in the process of "arriving".
Waiting for an Image
As you might expect, Java has a class that allows you to monitor the loading of Images from your server: the MediaTracker class. Using the MediaTracker class is easy:
- Create a MediaTracker object:
MediaTracker mt = new MediaTracker(this);
- Ask the MediaTracker to monitor your Images by using the addImage() method:
mt.addImage(img, 0);
(The second argument to addImage() is an ID used to track the progress of the loading. If you want to monitor several Images independently, then give each Image a unique ID. If you just want to wait until all of your Images are loaded, you can give them all the same ID.)
- Use a try-catch block to wait until your Image has arrived:
try
{
mt.waitForID(0);
}
catch (InterruptedException ie)
{
ie.printStackTrace();
}
Drawing Images
Once your applet has a valid Image object, you can use the Graphics drawImage() method to display it. There are several different versions of drawImage(), but the two you'll use most often are:
drawImage(img, x, y, this);
and
drawImage(img, x, y, width, height, this);
Most of the arguments are self-explanatory. The last argument, this, is required so that the Image can notify the applet as it is loading. If you use the MediaTracker class, you don't really use this extra notification, so you can pass null if you like.
When you call repaint(), the update() method is first called. The built-in update() method erases the background and then calls the paint() method. If you don't want the background erased, you can overrided update(), and simply call paint() without erasing the background. Of course, that means when you redraw your image in a new position, the old image will still be there, which isn't good.
To get around this, you must create a second, "in memory", or "offscreen" image (often called a "double-buffer") and do your painting on this offscreen buffer. Once all of your painting is finished, call drawImage() one last time, this time using the offscreen image, and the "real" Graphics object.
For more information on double-buffering, and overriding the update() method, check out Lesson 11.4, Animation, and pay special attention to FlyingTiger6.java.
Moving and Sizing Your Image
In the PixTrix applet, you'll need to write some methods to move and/or resize your image. Before you start writing code, make sure you understand the basic plan:
- The PixTrix applet will use several variables to control the position and size of your image. The int variables ulx, uly, width, and height control the upper-left corner (ulx, uly) and the width and height when painted. The Point variable, origin, is used when moving your applet, and the two boolean variables, isMoving and isSizing, are used to determine which action to carry out.
- When the mouse button is clicked, you need to determine if it was clicked over one of the sizing handles (meaning the user wants to resize), over the middle of the image (meaning the user wants to move the image), or over neither, in which case you do nothing.
- You'll set the two boolean variables--isMoving and isSizing--when the mouse is clicked, to tell the rest of the program which action to perform. If the user wants to move the image, you need to initialize the origin variable to the x,y arguments passed to this method as well.
- When the user drags the mouse, you'll check the boolean variables isMoving and isSizing to see what to do.
- If the user is sizing, you need to adjust the values of width and height when sizing using the two bottom handles. You'll also need to adjust height and width if the user resizes with the top two handles, but you'll also need to adjust ulx and uly as well.
- If the user is moving, then you need to see how far they've moved from the point where they first clicked the mouse, (the origin), and then adjust the ulx and uly fields accordingly. You don't need to adjust height and width at all, but you do need to update origin after ulx and uly are taken care of.
- When the user releases the mouse button, you merely need to set both isSizing and isMoving to false, so mouse drags won't be recorded.
That's all there is to it. Ready? Let's go.
Step-By-Step Instructions
Step 1
Define your applet's attributes. Start by creating a new applet named PixTrix. Your applet will need to import the java.awt, java.applet, and java.awt.event packages. We will need to respond to both mouse clicks and mouse movement, so you'll need to implmenent MouseListener [like Assignment 10], and MouseMotionListener [like Assignment 2].
Once you've set up the initial skeleton for your applet, add the following eight fields:
- Add two private Image fields, img.and oimg. We'll use oimg for a buffer.
- Add two private int fields to represent the point where your image will be drawn. I call them ulx and uly, which stands for "upper-left-x" and "upper-left-y".
- Add two more private int fields to represent the width and height of your image. Calling them width and height is not a bad idea. Initialize both of them to 25 (the initial size of your image).
- Add a pair of boolean fields, isSizing and isMoving. As their names suggest, these will be used to differentiate between moving your image and sizing it. Initialize them both to false.
- Add a private Point field called origin. This will be used when you move your image. Make sure you initialize it (0,0 is fine) when you create it.
After you've added these fields, add the necessary "dummy" methods for MouseListener and MouseMotionListener, and then make sure your code compiles before you continue. You can find the complete list of necessary methods in the recommended textbook.
Step 2
Write an init() method. Inside the init() method, carry out the following tasks:
- Set the background color of your applet to white
- Retrieve the "image" parameter from your HTML file using getParameter(). If getParameter() returns null, add a Label explaining the problem to your applet, and return.
- Load your image using the two argument form of getImage(), passing getDocumentBase() as the first argument, and the image parameter you retrieved from your HTML file as your second.
- Create a MediaTracker object, and use addImage() to let the MediaTracker monitor your image. Use an ID of 0 for your image.
- Inside a try-catch block, use the MediaTracker waitForID() method to wait until your image loads.
- Use addMouseMotionListener() and addMouseListener() to enable mouse events for your applet.
Initialize ulx and uly by using the following algorithm:
- Call getSize() to get the size of your applet.The getSize() method returns a Dimension object with two fields: width, and height.
- Subtract the value of your applet's width field [which you initialized to 25] from the width of your applet [which you determined by using the getSize() method], and divide the result by 2. Assign the finished result to ulx.
- Subtract the value of your applet's height field from the height of your applet, returned by using the getSize() method, and divide the result by 2. Assign the finished result to uly.
When you're finished, make sure your code still compiles. You can't really run the applet at this point because there is no visible output, but you will be able to do so after the next step.
Step 3
Write your applet's paint() method. Because we are using double-buffering, the PixTrix paint() method might look a little complex. In reality, however, it is pretty straitforward if you just take it step-by-step:
- Check to make sure that img is not null. If it is null, simply return.
- Check to see if oimg is null. If oimg is null then use the createImage() method to initialize it to the width and height of the applet. (Use getSize().width and getSize().height.)
- Create a local Graphics variable, named og. Initialize it by sending the getGraphics() message to oimg. [This is the "in memory" or "offscreen" Graphics object, you'll use to paint.]
- Using og, call the clearRect() method to erase the background.
- Draw your image using og and the img, ulx, uly, width, and height fields. For the last argument to drawImage(), pass this or null.
- Use og and fillRect() to draw a 4-pixel-wide rectangle over each of the corners of your image. These will be the "sizing handles".
- Draw the offscreen image on the actual screen by calling drawImage(), using the Graphics object g [not og], and passing oimg as the first argument [not img]. Draw at position 0,0. You don't need to pass the width or height.
- Release the local Graphics object og, by calling its dispose() method.
Think carefully when you draw your sizing handles, because you'll need to do some calculations to get the correct x,y values. You can't, for instance, simply draw the upper-left sizing handle at ulx, uly, and then draw the upper-right corner sizing handle at ulx+width, uly. If you do that, the upper-left handle will overlap your image, while the upper-right handle will appear sitting beside the image.
Once you've finished this step, compile your program and run it in Appletviewer. Your image should appear in the center of your applet, drawn at 25 x 25, and you should have four sizing handles, one at each corner, like this:
Step 4
Write code to handle the initial mouse click. Put this in the mousePressed() method.
Inside the mousePressed() method, complete the following steps:
- Create a new Rectangle object using ulx, uly, width and height. Name your Rectangle r.
- Shrink the Rectangle r by 6 pixels in each dimension by sending it the grow() message. (I use 6 pixels even though the sizing handles are only 4 pixels wide, because it is hard to "hit" a 4-pixel square with the mouse.) The grow() method takes two arguments--the amount to increase in the horizontal (x) dimensions, and the amount to increase in the vertical dimension. Because you want to actually shrink your Rectangle object r, instead of increasing the dimensions, use -6 for both arguments passed to grow().
- Check to see if the point where the mouse was clicked was inside your rectangle. Use the MouseEvent getX() and getY() methods to determine the point where the mouse was clicked. To determine if the point lies inside your rectangle you use the Rectangle contains() method.
- If the point is inside your rectangle, it means that the user wants to move, because they couldn't have clicked along the outside where the sizing handles are located. [That's because we excluded the region where the sizing handles were located, when we used the grow() method.] If this is true, then set the variable isMoving to true, and initialize the Point field, origin, to the position where the mouse was clicked. [Make sure you don't inadvertently create a local Point variable named origin when you do this; that's very easy to do.]
- If the point does not lie inside your rectangle, [as reported by the inside() method], then you must check to see if it lies in the 4-pixel border around your image where the sizing handles were drawn. To test this, simply call the grow() method again, [passing +8 as your arguments this time], and check using contains() as in the previous step.
- If your point does lie in the 4-pixel margin (or int the 2-pixel "slop" on each side") surrounding your image, then set the variable isSizing to true.
Recompile your program once again.
Step 5
Write code to handle the mouse release. This is considerably simpler than handling the initial mouse press. All you have to do is set both the isSizing and isMoving fields to false. Put this inside the mouseReleased() method.
Step 6
Write code to handle the mouse dragging. This is where you'll actually move or size your image. Take it step-by-step, and you should have no problems.
Put this inside the mouseDragged() method. Inside your method, follow these steps:
- Write an if-else if control structure. One block should execute when isSizing is true, the other when isMoving is true. If neither is true, you won't do anything.
- Inside the isMoving block, you want to change the values in ulx, uly, and the Point origin. Here's how:
- Subtract origin.x from the current mouse x position. You get the mouse's x position using the MouseEvent getX() method. The result of this calculation tells you how far you've moved the mouse in the horizontal direction since you first clicked it. Change ulx by adding the result of this calculation to ulx. [Hint: use +=]
- Now, do the same thing for uly. You'll subtract origin.y from the mouse position y, and add the result to uly.
- Next, update the Point origin with the current mouse position.
- End the section by calling repaint() so that your picture is redrawn in its new position.
- Inside the isSizing block, you'll have another pair of nested if-else if structures. This is necessary to determine whether only height and width need to be changed, or if we also need to change ulx and/or uly as well. Here's how how do it:
- Add a local int variable named midx, and initialize it to ulx + width / 2. This is the horizontal center of your image. Add a second local int variable named midy. Initialize it in a similar manner, but use uly and height. This is the vertical center of your image.
- The first if-else if checks whether x, (the current mouse position), is less than midx or greater-or-equal. If it is greater-or-equal, then you only need to adjust width by setting it equal to (x - ulx). If x is less than midx, however, you need to a.) find the difference (ulx - x), b.) add the difference to width, and c.) set ulx equal to x.
- The second if-else checks to see if y is less than or greater than midy. If greater, just set height equal to y - uly. If less than, follow the same steps you did for the horizontal: a.) calculate the difference (uly - y), b.) add the difference to height, and c.) set uly equal to y.
- After you're finished adjusting the the various fields, call repaint(), and your image will be redisplayed at its new size and position.
Recompile and run your program. You'll notice that there is still considerable flicker, even though we've implemented "double-buffering". Fortunately, the flicker is easy to fix.
Step 7
Write code to handle the update() method. The built-in update() method erases the screen and then calls paint(). Since we're redrawing the entire buffer, we don't need the automatic erasing. In the update() method, which takes a Graphics argument, just like paint(), simply call the paint() method, passing the argument along.
Finishing Up
Post your applet. Once you've run your test applet locally, add it to the Assignment 11 HTML file on your Web site. Remember that you must send the .class file for the PixTrix class, as well as an updated HTML file. When you use FTP to send up your .class files, remember that they must be transferred using binary mode, not ASCII.
|