Lab 3 - A Picture Drawing Program
Lab 3 -- A Picture Drawing Program
Principles of Computer Science
Spring, 2005
Overview
In this week's lab, you will be writing an interactive drawing program, based
on the simple shapes (circle, square) you used in lab 2. The difference
here is that instead of directing the shapes to move with method calls, the
program will set up a framework in which an interactive user can create shapes
and move them around with a mouse.
The application program will present the user with a drawing canvas. The
user can create circles and squares and then drag them around on the canvas.
In addition, the user will be able to change the color and size of the
circles and squares. The picture drawn by the user can then be
saved into a file.
Here is the jar file for this lab.
Java's event model
We have some experience with handling events in Java from last week's lab. You'll recall that we associated with each button an action method, to be executed whenever the button was clicked. Mouse and keyboard events are handled in a similar way.An object can register with Java as a "listener" for mouse and keyboard events. This means that it wants to be notified whenever such an event occurs. Then whenever, say, a key is pressed by the user, all the keyboard listeners are notified. Java notifies the listeners by calling the "keyPressed" method of each one. (A keyboard listener is required to have a method called "keyPressed".) Similarly, a mouse listener has methods for events such as "mousePressed" and "mouseClicked", and a mouse motion listener has methods for "mouseMoved" and "mouseDragged".
In order to write a program which can detect keyboard and mouse events and respond to them, a programmer must do the following:
1. Designate one or more classes as listeners for mouse or keyboard events. This is done through the clause "implements MouseListener" or "implements KeyboardListener" in the class header. (A single class can be both a mouse listener and a keyboard listener.)
2. For each listener class, the program must provide appropriate methods which will be called as a result of interactive events. The table below lists the required methods:
| type of listener |
required methods |
| MouseListener |
public void mouseClicked(MouseEvent e); public void mousePressed(MouseEvent e); public void mouseReleased(MouseEvent e); public void mouseEntered(MouseEvent e); public void mouseExited(MouseEvent e); |
| MouseMotionListener |
public void mouseMoved(MouseEvent e); public void mouseDragged(MouseEvent e); |
| KeyListener |
public void keyPressed(KeyEvent e); public void keyReleased(KeyEvent e); public void keyTyped(KeyEvent e); |
3. When objects of the listener classes are instantiated, they must register as listeners by calling the "add....Listener" method of the GUI component they are interested in.
In this lab, you'll be writing a listener class which includes all the methods in the table. The program will associate a listener object with each circle or square object created by the user. The input listener listens for mouse and keyboard events, and takes action in response to these, mostly be sending messages to its associated shape object.
Getting Started
You can get started by downloading an archive file ( lab3.jar ) which contains the classes you'll need to complete the lab. The jar file contains:- Circle.java -- a modified version of the Circle class from lab 2.
- Square.java -- a modified version of the Square class from lab 2.
- CircleInputListener.java -- responsible for listening for keyboard and mouse events, and using these to control the position, size, and color of a circle.
- SquareInputListener.java -- responsible for listening for keyboard and mouse events, and using these to control the position, size, and color of a circle.
- Canvas.java -- as before;
- PictureMaker.java -- the application class, which includes the main method.
- Instead of keeping an xPosition and yPosition, the position of the object is specified by a Point object. A Point object represents an (x,y) pair of coordinates. For the circle, the point is the center of the circle. For the square, the point is the upper left hand corner of the square. You can obtain the x- and y-coordinates of point with the methods getX() and getY(), both of which return values of type double.
- In Circle, the size instance variable (which was actually the diameter of the circle) has been replaced by one representing the radius of the circle. In Square, size has been replaced by two variables, one for width and the other for height. (So the square is not really a square, it is a rectangle. But the name for this class is still "Square". )
- In Circle, methods called "grow()" and "shrink()" have been added which increase or decrease the radius of the circle by a fixed amount. In Square, methods "growHorizontal", "growVertical()", "shrinkHorizontal()", and "shrinkVertical()" have been added to increase or decrease the width or height of the square.
- The constructors have been modified so that when a Circle or Square is instantiated, a new CircleInputListener or SquareInputListener is also instantiated.
Circle() -- Each circle needs to have an input listener which will listen for key and mouse events, which can then be relayed to the circle. This input listener is created by the statement
new CircleInputListener(this);The reference "this" (which is a reference to the Circle) is passed to the constructor of the input listener.
CircleInputListener(Circle circle) -- Each input listener needs to know which Circle it is responsible for. This circle is a parameter of the constructor; a reference to the circle is saved by the constructor in an instance variable with the statement
this.circle = circle;The input listener must also register with Java's event handling mechanism as a KeyListener, a MouseListener and a MouseMotionListener. It does so with the statements.
Canvas.getCanvas().addKeyListener(this);
Canvas.getCanvas().addMouseListener(this);
Canvas.getCanvas().addMouseMotionListener(this);
You're now ready to begin writing your version of the Picture Maker program.
Part one. Creating objects.
When the mouse is clicked on the drawing canvas, your program should create a
new object. Clicking on the left button will produce a circle whose center
is at the position of the mouse. Clicking on the right button will produce
a square whose upper left hand corner is at the position of the
mouse.
When a mouse event (mouse clicked, pressed, released, dragged, or
moved) occurs, a MouseEvent object is created and passed as a parameter to the
corresponding action method of each listening object. You can read a
description of MouseEvent in chapter 16 of the text (see p. 591-597), but you
don't need a detailed knowledge of MouseEvent for this lab. Thre are really just
two feature of this class that you will use. One of the methods of MouseEvent is
getButton, which returns an integer code representing which button was
involved in the event. This code can take on three possible values:
MouseEvent.BUTTON1, MouseEvent.BUTTON2, and MouseEvent.BUTTON3,
representing the left, middle, and right mouse buttons. MouseEvent also has a
Point attribute which represents the (x,y) position of the mouse when the event
occurred. This can be obtained by calling the method getPoint.
(getPoint has no arguments and returns a Point object.)
For this step, you are to fill in the body of the "mouseClicked" method
in the PictureMaker application class. It should check to see
which button was clicked. If the left button was clicked, create a circle
and make it visible (remember that the statement c = new Circle(p) makes a new,
invisible circle centered at point p and assigns it to variable c). Use
the getPoint() method to determine the center of the circle. If the right
button was clicked, create a square and make it visible. Use the getPoint()
method to determine the upper left corner of the square.
Try
the program out now by re-compiling the PictureMaker.java file. You should
be able to draw circles and squares on the canvas.
Part two. Dragging objects.
A. Circles
To achieve the effect of dragging an object around on the screen, we need to write action routines for the InputListener classes that will be called when the mouse is pressed, dragged, and released. These are in the CircleInputListener.java file. Remember that every listener of a given type is called when the appropriate interaction happens. For example, when a MousePress event happens the mousePressed() method of every MouseListener will be called. We need to start out making most of the listeners ignore this call.
A1. The first thing the mousePressed() method needs to do is to determine whether the mouse position is inside the circle or square the mouse is attached to. Go to file Circle.java and add the following method to the Circle class:
boolean contains(Point p) // returns true if p is inside the circle, false if it is not
You can compute this by testing whether the distance from p to the center of
the circle is less than the circle's radius. The Point class has a method called
distance: p.distance(q) is the distance from Point p to Point q as a value of
type "double".
Now go to CircleInputListener.java and write the mousePressed(), mouseDragged(),
and mouseReleased() methods:
A2. The mousePressed() method
- Recall that each listener object controls a single shape object. The listener can safely ignore mousePressed events which occur when the click falls outside the listener's object. So, the first thing that needs to be done here is to test whether or not the mouse has been pressed within the listener's shape object. This can be done by applying the object's contains method to the current mouse position.
- Save the current mouse position in the corresponding instance variable.
- Note that the mouse is currently dragging the object by setting the isDragging variable to true.
This event indicates that the mouse has moved while being held in the down position. When it occurs, the listener responds as follows:
- The listener must first determine if the mouseDragged event applies to its object. In this case, it does not depend on the current mouse position; a drag operation might take the mouse outside of an object being dragged. What is important instead is the "isDragging" boolean variable. If "isDragging" is true, then this object needs to be moved to a new position; otherwise, the event can be ignored.
- If the object needs to be moved, the listener must determine where to move
it, as follows:
- Get the new mouse position by calling the getPoint method of the MouseEvent.
- Use the moveTo(Point p) method for circles to redraw the circle at the mouse position.
- It will be useful later (when we get to keyboard events) to have the position variable in the listener maintain the current mouse position, so whether you move the circle or not, use the getPoint method to update the class variable position.
The isDragging variable should be set to false to indicate that the object is no longer being dragged by the mouse.
When you've completed these three methods, try your program out again.
You should now be able to create objects and drag them anywhere on the
canvas.
Note that one "feature" of the program is that if several objects
are under the mouse cursor when it is pressed, all of them will be dragged
together. An alternative interface would be to drag only the topmost
object in that situation. However, that would require keeping track of a
depth parameter for each object, which we are not doing. In addtion, there
would need to be some way to allow the user to modify the depth of objects
interactively, so that an object in front of another could be moved behind it,
etc.
B. Squares
You will need to make similar changes in the Square and SquareInputListener classes to implement dragging for square objects. For the SquareInputListener class you will need to add bodies to the mousePressed(), mouseDragged() and mouseReleased() methods. In file Square.java you need to add methods contains(). This is a bit different from the contains() method for circles because the geometry of a square differs from that of circles:
. Note that a point lies within a rectangle if:
- its x-coordinate is within the bounds of the horizontal limits of the rectangle. For a rectangle whose upper left corner and width are known, the horizontal limits are corner.x and corner.x+width, as shown in the figure below. (Note: the Point class contains two instance variables x and y representing the x and y coordinates of the point.)
- its y-coordinate is within the bounds of the vertical limits of the rectangle. For a rectangle whose upper left corner and width are known, the vertical limits are corner.y and corner.y+height.

Use this picture as a guide to writing code for the Square contains() method. Remember that you can get the x and y coordinates of a point with p.getX() and p.getY(). Your method might look something like this:
public boolean contains(Point p) {
if (p.getX() < corner.getX())
return false;
else if (p.getX() > corner.getX() + width)
return false;
else .....
else
return true;
}
After you implement these changes and recompile, you should be able to create and drag both circles and squares.
Part three. Changing sizes and colors.
We have added to both the Circle class and the Square class a no-argument changeColor() method that cycles object colors around a standard list of predefined colors.
public void changeColor()
Note that there is also a changeColor method that takes a String argument: public void changeColor(String newColor); these methods are different because they take different arguments.
Now we'll add a feature to the program which will allow the user to change the size or color of objects on the screen. We'll use the space bar and arrow keys for this purpose, as follows:| key |
action |
key code |
| space bar |
change color |
KeyEvent.VK_SPACE |
| up arrow | grow (circle); shrink vertical (square) |
KeyEvent.VK_UP |
| down arrow |
shrink (circle); grow vertical (square) |
KeyEvent.VK_DOWN |
| left arrow |
shrink horizontal (square only) |
KeyEvent.VK_LEFT |
| right arrow |
grow horizontal (square only) |
KeyEvent.VK_RIGHT |
I've added
grow and shrink methods to Circle and growHorizontal, shrinkHorizontal,
growVertical, and shrinkVertical methods to Square that you can use
here.
The method that you need to write is the keyPressed method in the
CircleInputListener and SquareInputListener classes. Here are some
guidelines:
1. Under what circumstances should a shape
respond to a key press? We'll follow the rule that the only shapes to
respond are those which lie under the mouse cursor when the key is pressed.
You can test this by applying the shape's contains method to the current
mouse position. (This means that the listener must keep track of the mouse
position, even when no drag operation is in effect. We'll add that code
below.)
2. The method needs to know what key was
pressed, in order to decide what action to take. A code indicating which
key was pressed can be obtained from the KeyEvent object that is passed to the
keyPressed method. KeyEvent has a method called "getKeyCode", which
returns an integer value representing the key code. By calling this
method, you can tell which key was pressed.
3. The key codes for the five keys we are interested in are
shown in the table above. Your method should use a series of ifs to determine
if any of these five were pressed, and if one was, take the appropriate action.
The actions, such as grow(), shrink(), and so forth, are for the most part already
implmented as methods of the Circle and Square classes.
4. Next, consider the question of how to keep
track of the current mouse position if the mouse is not being dragged.
(The mouseDragged method is already recording the mouse position when the
mouse is dragged.) This can be done by writing a "mouseMoved" method which
gets the current mouse position and saves it in the "position" instance
variable.
When you have finished writing the keyPressed and mouseMoved
methods, try out your program again. You should now be able to control the
size and color of your shapes.
Congratulations! You are now finished with lab 3. The directory you submit should contain all the .java and .class files for your completed application.
On your own (optional)
Here are some enhancements you can try. These are completely optional.
1. In Canvas.java, you'll find a method called windowClosing. Within
that method, there is a section of about 15 lines which has been commented out.
Restore that section by removing the comment markers /* and */.
You'll also need to uncomment a line at the top of the file which says
import javax.imageio.*;
Run the program again. When you close the application window, you'll be given the opportunity to save your picture in a file. The picture will be stored in PNG format. Test it out by drawing a picture or two and saving them into png files.
2. Modify the Circle class so that it can be stretched horizontally or vertically
as independent operations; that is, make it into an ellipse. Drawing an
ellipse is not difficult. In fact, if you look at the draw method in Circle.java,
you'll see that it actually draws an ellipse, but one whose horizontal and vertical
dimensions are the same. The tricky part here will be to write a version
of the contains method that will work for an ellipse.
3. Implement a Triangle class than can be used with PictureMaker. Most
of the code can be copied from Square, but you'll need to copy the draw method
from the Triangle class from lab 2. You'll need to provide the user with
a way to create triangles (a mouse click while the shift or control key is held
down would work). You'll also need to decide how to determine if a point
lies within a given triangle (i.e., a contains method).