Graphics

advertisement
COMP 14
Prasun Dewan1
13. Graphics
In this chapter, you will learn how to create and animate graphics. Graphics is not conceptually
complicated. There are low-level details you would need to master if you were to implement graphics using
Java libraries. Fortunately, ObjectEditor can hide you from these details. So you will put on its training
wheels again, and take them off later in a couple of chapters when you learn details of Java toolkit libraries.
This way you can separate the concepts from the details, as you did when creating textual user-interfaces.
The examples you will see in this chapter require the use of two predefined Java types, Vector and
Enumeration. Vector provides a powerful built-in facility for defining dynamic collections. Once
you learn how to use it, you will probably never use an array again. A Vector is much like the dynamic
collections you saw earlier such as history, database, and set – it provides many of the operations offered
by these collections plus some additional ones. It is built on top of the type Enumeration, which is like
the CharEnumeration type you saw earlier - instead of enumerating characters, it can enumerate
arbitrary objects. Therefore, learning how these two types work will be trivial – in fact, you can probably
easily implement most aspects of them at this point. The only subtlety has to do with casting values
retrieved from them to the right type. Moreover, they will solidify your understanding of the basic idea of
dynamic collections and enumerations.
Square with Text
Figure 1 is an example of a graphics-based user interface. It displays a square with a text box centered in it.
The square can be moved and resized, but the text box cannot be edited directly. If we move or resize the
square, the text box is repositioned to stay centered in the rectangle.2
 Copyright Prasun Dewan, 2000.
To move the rectangle, you can press the select radio button and drag it with the left mouse button
pressed. If the Immediate toggle button is pressed, then the text box moves as the rectangle is dragged.
Otherwise, it moves after the user releases the left mouse button.
1
2
1
Figure 1 Text Stays Centered as Square Moves
The user-interface, as described above, looks trivial. When we think of implementing it, however, it seems
much more complicated, as we must consider several details such as how the square and text box should be
drawn on the screen and how the move and resize commands should be implemented, worrying about how
mouse dragging events should be intercepted and translated into position and size changes. Fortunately,
ObjectEditor allows us to ignore these details, because built into it is a graphics editor that displays several
geometric shapes such as points, lines, squares, ovals, and text boxes, and provides commands to move and
resize them. All we have to worry about is what shapes we want displayed on the screen, and what their
sizes and positions should be.
Pixels and Coordinate System
In order to specify sizes and positions, we need to understand the coordinate system available to us. As it
turns out, the coordinate system familiar to us from Mathematics cannot be used directly. In the
Mathematics coordinate system, we can specify continuous values along the X and Y dimensions, locating
a point with arbitrary precision. As we saw earlier, computer numbers cannot model real numbers
precisely. More important, the computer cannot draw at arbitrary locations on the screen. It can draw only
at a subset of the infinite points on the screen called the screen pixels.
2
(0,0)
(3,2)
X
pixels
Y
Figure 2 Java Coordinate System
These are the points the hardware drawing gun colors to draw on the screen. The pixels are equally spaced
in both the X and Y directions. The pixel spacing or pixel unit determines the resolution of the screen – the
higher the resolution, the smaller the pixel unit. If the pixel spacing is p (in some unit of length such as
millimeters) on a screen with width W and height H, then the resolution of the screen is W/p by H/p.
The coordinates of a point are given in pixel units. Thus, the coordinate (x, y) indicates a pixel that is x
pixel units from the origin in the X direction and y pixel units from the origin in the Y direction. Since
graphics coordinates refer to discrete rather than continuous points on the screen, they are specified as int
values. Because they are specified in pixel units, the actual location of a point, (x,y) depends on the
resolution of the screen. Thus, if on a particular screen, the point (256, 174), is at a location 10cm from the
Y axis and 8 cm from the X axis, then on a screen of the same size with twice the resolution, it will be
20cm from the Y axis and 16 cm from the X axis.
Each window is associated with its own coordinate system so that we can create shapes independently in
different windows. The origin of the coordinate system is the upper left corner of the window- X values
increase from left to right; and Y values increase from up to down and not down to up as we are used to
from Mathematics. The reason for not following the Mathematics convention is that it is usual to think of
the upper left corner of a window as its position and specify positive coordinates, relative to this corner, for
points within the window.
Specifying Shapes
With this coordinate system, we can describe arbitrary two-dimensional shapes. In general, a shape can be
fairly complex, and the means for specifying thus, can also get arbitrary complex. If we restrict ourselves to
lines, rectangles, and ovals, however, we can use the following simple mechanism for specifying a shape.
3
x2, y2
x1, y1
h2
h1
w1
w2
x3, y3
h3
w3
Figure 3 Specifying Shapes Using Rectangular Bounds
A rectangle described by the tuple (x, y, w, h), where (x, y) are the coordinates of its upper left
corner, and w and h are its width and height, respectively. The pair (w, h) is called the size of the rectangle.
An oval or line is described by specifying the rectangle that bounds it. Thus, the rectangle, line, an oval of
Figure 3 are described the tuples (x1,y1,w1,h1), (x2,y2,w2,h2), and (x3, y3, w3, h3) respectively. Both the
width and height can be negative, in which case, (x,y) is not truly the upper left corner of the rectangle. The
above tuple defines a rectangle in which one corner it at position (x, y) and the diagonally opposite corner
is at position (x+w, y+h).
Graphical Constraints
We are now ready to define the user-interface of Figure 1. Though it looks very different from textual userinterfaces seen so far, in fact, it is very similar to the spreadsheet user-interfaces. Compare it with the BMI
spreadsheet. Just as the BMI spreadsheet defines a couple of editable items, the height and weight, and a
read-only item, the BMI; this user-interface also defines an editable item, the square, and a read-only item,
the text. Moreover, just as the spreadsheet defines constraints among the displayed items, automatically
updating the BMI when the height or weight is edited, this user-interface automatically updates the text box
when the square is edited. The only difference is that, in this user-interface, constraints are defined among
graphical rather than textual elements.
In fact, we can represent graphical elements as read-only or editable properties of an object and use getter
and setter methods to implement the constraints among them, leaving the display and editing of these
properties to ObjectEditor, as we did for textual elements. Figures 3 and 4, the implementation of the
example user-interface, show how.
Figure 3 gives the interface, SquareWithText, of the object representing the square with centered text.
The square is described by the class, shapes.RectangleModel, while the text is described by the
class, shapes.TextModel. Neither of these classes is part of the standard Java library; they are defined
in the library shapes.jar we included in the class path. The reason for adding the suffix, “Model”, to
their names will be clear when we study the Model/View/Controller framework in the next chapter. We use
the class TextModel instead of String to describe the text, because unlike the latter, it describes text
that is displayed in a potentially movable and resizable textbox in a graphics window instead of a static text
field in a form-based ObjectEditor window we have seen in the previous chapters. Similarly, we use the
4
class RectangleModel instead of the Java class Rectangle to describe a rectangle that is represented
graphically rather than textually as a list of form items displaying its properties.
import shapes.RectangleModel;
import shapes.TextModel;
public interface SquareWithText {
public RectangleModel getSquare();
public void setSquare(RectangleModel newVal);
public TextModel getText ();
}
Figure 4 Interface SquareWithText
Thus, the getter and setter methods, getSquare() and setSquare(), describe the editable square
shape, while the getter method, getText(), describes the read-only text displayed in the graphical
window.
import shapes.RectangleModel;
import shapes.TextModel;
public class ASquareWithText implements SquareWithText {
static final int INIT_X = 10;
static final int INIT_Y = 10;
static final int INIT_SIDE_LENGTH = 100;
static final String TEXT_STRING= “hello”;
RectangleModel square;
TextModel text;
public ASquareWithText() {
square = new RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH,
INIT_SIDE_LENGTH);
text = new TextModel(TEXT_STRING);
placeText();
}
void placeText() {
text.setCenter(square.getCenter());
}
public RectangleModel getSquare () {
return square;
}
public void setSquare (RectangleModel newVal) {
square = newVal;
placeText();
}
public TextModel getText () {
return text;
}
}
Figure 5 Graphical Constraints
Figure 4 gives the class, ASquareWithText, implementing this interface. It declares separate instance
variables for the two properties. The getter methods simply return the values of these variables. The setter
method for the square, called when the user edits the square, sets the value of the square instance variable
to the edited value, and calls the method placeText(); which in turn, uses the getCenter() and
setCenter() methods defined by the two shapes to center the text in the edited square. Finally, the
constructor assigns initial values to the two instance variables. As we can guess from the code, the
constructor for RectangleModel , like the Rectangle constructor we saw earlier, takes as arguments
bounds of the rectangle (x, y, width, height); and the constructor of TextModel takes as an argument the
5
string to be displayed in the text box. A textbox automatically computes its bounds from the string it
contains, which can be changed later, as discussed below.
Thus, the implementation of this example is fairly straightforward, and more important, follows the pattern
of the code we saw in Chapter 3. The only new feature we learned here the classes representing objects
displayed graphically.
Some AWT and ObjectEditor Graphics Classes
The example above shows only some of the graphics classes we can use. Some of the graphics classes are
provided by the package java.awt, which is part of the Java distribution. Some, like
RectangleModel, are provided by the package shapes, which is part of the ObjectEditor distribution.
We will refer to the former as AWT classes and the latter as ObjectEditor classes.
Let us first consider some of the AWT graphics classes. The class Rectangle describes the rectangular
bounds of a shape. It provides a constructor that takes the four integers as arguments and creates a new
instance representing the these bounds:
Rectangle r = new Rectangle(x,y,width,height);
It also allows us to retrieve and change components of these bounds. The methods getLocation() and
setLocation()can be used to retrieve and set the location of the upper left (north west) corner of the
rectangle, while the methods, getSize() and setSize() can be used to change the size of the
rectangle. A location is represented by an instance of the class Point, which provides a constructor that
takes the x and y coordinates of the location as arguments and constructs a new instance representing the
point:
r.setLocation(new Point(x, y));
Similarly a size is represented by an instance of Dimension, which provides a constructor that takes the
width and height of the rectangle as arguments and constructs a new instance representing the size:
r.setSize(new Dimension(width, height));
Unfortunately, these classes do not provide getter and setter methods for setting all components. The class
Point provides public access to the two coordinates by defining the public variables, x and y. Similarly,
Dimension defines the public variables, width and height, and Rectangle defines the public
variables, x, y, width and height. The x and y variables of a Rectangle provide access to the
coordinates of its upper left corner, that is, its location.3
Suppose we execute the following code:
Rectangle r = new Rectangle(0, 5, 50, 100);
r.x = 10;
r.width = 150;
Dimension d = r.getSize();
Point p = r.getLocation();
Then the following four expressions evaluate as shown below:
p.x  10
p.y  5
d.width  150
d.height  100
Another useful AWT class is Color, which provides the following static public named constants, black,
red, pink, orange, yellow, green, magneta, cyan, and blue, for representing different colors.
Again, the designers of this class violated one of our conventions – the constants are named using
lowercase letters rather than upper case letters.
3
Exposing components as public variables does not make it possible, for instance, to change the Cartesian
representation of a Point to a Polar one.
6
The ObjectEditor graphics classes are built on top of these AWT classes. Besides TextModel and
RectangleModel, three other classes are provided by ObjectEditor, PointModel, LineModel and
OvalModel, for specifying points, lines and ovals that are represented graphically. Like the constructor of
RectangleModel, these types offer constructors that take as arguments the rectangular bounds of the
shape to be created. For instance, the expression:
new LineModel(5, 10, 30, 40)
creates a line between the upper left corner and lower right corner of a rectangle whose upper left corner is
(5, 10) and whose width and height are 30 and 40 respectively; and the expression:
new OvalModel(5, 10, 30, 40)
inscribes an ellipse in the same rectangle.
Once we have created one of these shapes, we can retrieve and set the components of its bounds using the
methods getLocation(), setLocation(), getSize(), and setSize(), which work the same
way as the corresponding methods provided by Rectangle. These methods are sufficient to retrieve and
manipulate any aspect of the bounds of the shape. However, using them may require some calculations on
our part. For instance, if we wish to move the center of a shape to the center of another shape, we would
have to calculate the center of the latter from its upper left corner, and then calculate the upper left corner
of the former from its center. To spare us from such calculations, the shape classes provide several
additional convenience methods such as getCenter() and setCenter() we saw above to access
various aspects of the shape.
The following are some of the methods provided by all of the ObjecEditor shape classes:
 getX(), setX(), getY(), setY(), getWidth(), setWidth(), getHeight(),
setHeight(), which get and set int values.
 getCenter(), setCenter(), getNWCorner(), setNWCorner(),
getNECorner(), setNECorner(), getSWCorner(), setSWCorner(),
getSECorner(), setSECorner(), which get and set instances of (AWT) Point
representing the center and four corners of the bounding rectangle.
 isFilled() and setFilled() to set a boolean property indicating if the shape should be filled
when drawing it.
 getColor() and setColor() to get and set instances of Color representing the color of the
shape.
When you try and invoke an operation on an instance of one of these classes, the J++ editor will show you
a menu of all available operations. The functionality of the operations will, typically, be clear from their
names, parameter types, and return types.
As you have probably noticed, the menu displayed by the J++ editor is very useful. Unfortunately, in the
process of displaying the menu, at least on my computer, it sometimes crashes, giving the following
uninformative error message, saying there is a pure virtual call:
When this happens, you will lose all changes you have made! Therefore, be sure to save changes frequently
if this ever happens to you. Also, to get around this problem, you will have to make sure that J++ does not
7
try to display the menu. Sometimes saving a file disables menu generation. If this does not work, type right
to left. Assume we are trying to instantiate one of the shape model classes:
new RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH, INIT_SIDE_LENGTH);
After we type new, J++ tries to give a menu of all the available constructors . To prevent it from crashing,
type the constructor, with any parameters, first:
RectangleModel (INIT_X, INIT_Y, INIT_SIDE_LENGTH, INIT_SIDE_LENGTH);
and then enter the keyword new. Since the constructor is already is entered, it does not try to create the
menu.
It is important to distinguish between corresponding classes provided by ObjectEditor and AWT –
RectangleModel and Rectangle, and PointModel and Point. The main difference is that
ObjectEditor does not display instances of Point and Rectangle as shapes on the screen, while it does
so for RectangleModel, PointModel, and the other shapes classes it defines. The following figure
illustrates this, showing how an instance of Rectangle and a corresponding instance of RectangleModel are
displayed by ObjectEditor.4
Demonstrating Details of Graphics Classes
The following example demonstrates details of several of the AWT and ObjectEditor graphics classes.
It creates an object with five graphical components: a rectangle, line, textbox, oval, and point with the
following constraints:
 The bounds of the oval and rectangle are the same.
 The width of the rectangle, oval, line and textbox are the same.
 The center of the point is at the south west corner of the bounding box of the oval and rectangle.
 The northwest corner of the line is at the south west corner of this bounding box.
The empty boolean property of a Rectangle returns true if the width or height of a rectangle is equal to
or less than zero. ObjectEditor interprets zero and negaive values for width and height in the manner
described in the section of Specifying Shapes.
4
8

The center of the text model is at the center of the bounding box.
The object also allows the x coordinate and height of the bounding box to be changed, as shown in the
figure below. The user- interfaces discussed previously in this chapter have all had only graphical elements,
while the ones we saw earlier had only textual elements. ObjectEditor allows us to create user-interfaces
that have both textual and graphical elements, as illustrated by this figure. ObjectEditor displays all
properties of an object that are instances of its shape classes in the graphics window and all other properties
in the text window. We will refer to these two kinds of properties as graphical and textual properties. If
there is nothing to be displayed in either window, ObjectEditor does create the window.
The interface of the object displayed in the window above is given below.
import shapes.RectangleModel;
import shapes.OvalModel;
import shapes.LineModel;
import shapes.PointModel;
import shapes.TextModel;
public interface ShapesDemo {
public RectangleModel getRectangleModel();
public OvalModel getOvalModel ();
public LineModel getLineModel ();
public TextModel getTextModel ();
9
public PointModel getPointModel ();
public int getX();
public void setX(int newVal);
public int getHeight();
public void setHeight(int newVal);
}
All five graphical properties are associated with only getter methods. They are not associated with setter
methods because they cannot be edited directly by the user in the graphics window. The other two
properties are editable and are therefore associated with both setter and getter methods.
The implementation of this interface is long, but straightforward.
import shapes.PointModel;
import shapes.TextModel;
import java.awt.Rectangle;
import java.awt.Point;
import java.awt.Color;
import java.awt.Dimension;
public class AShapesDemo implements ShapesDemo {
public static int INIT_X = 0;
public static int INIT_Y = 0;
public static int INIT_WIDTH = 100;
public static int INIT_HEIGHT = 50;
int x = INIT_X; // x coordinate of (NW corner of) rectangle and oval
int y = INIT_Y; // y coordinate of rectangle and oval
int width = INIT_WIDTH; // width of rectangle, oval, line and text
int height = INIT_HEIGHT; // height of rectangle, oval and line
Point location = new Point(x,y); // location of rectangle and oval
//size of rectangle, oval, and line
Dimension size = new Dimension(width, height);
// bounds of rectangle and oval
Rectangle bounds = new Rectangle(location, size);
RectangleModel rectangleModel = new RectangleModel();
OvalModel ovalModel = new OvalModel();
LineModel lineModel = new LineModel();
TextModel textModel = new TextModel("Shapes Demo");
PointModel pointModel = new PointModel(0,0);
public AShapesDemo() {
ovalModel.setFilled(true);
ovalModel.setColor(Color.gray);
constrainShapes();
}
void constrainShaopes() {
location.x = x;
location.y = y;
bounds.setLocation(location);
size.width = width;
size.height = height;
bounds.setSize(size);
rectangleModel.setBounds(bounds);
ovalModel.setBounds(bounds);
lineModel.setSize(size);
lineModel.setNWCorner(ovalModel.getSWCorner());
pointModel.setCenter(ovalModel.getSECorner());
textModel.setWidth(width);
10
textModel.setCenter(ovalModel.getCenter());
}
public int getX() {
return x;
}
public void setX(int newVal) {
x = newVal;
updateShapes();
}
public int getHeight() {
return height;
}
public void setHeight(int newVal) {
height = newVal;
updateShapes();
}
public RectangleModel getRectangleModel () {
return rectangleModel;
public OvalModel getOvalModel () {return ovalModel;
}
public LineModel getLineModel () {return lineModel;}
public TextModel getTextModel () {return textModel;}
public PointModel getPointModel () {return pointModel;}
}
}
The class creates instance variables for the seven properties. It also creates several instance variables such
as width and size for maintaining the constraints of the application and for demonstrating how values of
the AWT graphical classes are manipulated. Most of the variables are initialized completely while
declaring them. The only one that is not is ovalModel, whose filled property is needs to be initialized.
Therefore, the constructor of the class does this additional initialization.
The method constrainShapes() implements the constraints of the application, calling several of the
convenience methods provided by the ObjectEditor shape classes. This method is called by (a)the
constructor to ensure that the initial configuration obeys the constraints, and (b) each of the setter methods,
to ensure that the constraints are maintained after changes to the variables defining the constraints.
This example demonstrates all of the AWT and ObjectEditor classes we have introduced in this chapter:
Point, Dimension, Rectangle, Color, RectangleModel, OvalModel,
LineModel, TextModel, and PointModel. As this example shows, we must import the AWT
classes from the package java.awt and the ObjectEditor classes from the package shapes.
In the examples we have seen so far, the number of graphics elements displayed on the screen was fixed –
one square and one textbox. In the following example, we will see how to create a dynamic number of
graphics elements.
Exposing Dynamic Indexed Elements
Suppose we wish to enter and view a series of points in some two-dimensional space. For instance, we wish
to enter and view points on the trajectory of a rocket or the points in an exam curve. Figure 6 shows a userinterface that allows us to perform this task.
11
Figure 6 PointHistory
It provides an operation, addElement(), for adding a new point whose x and y coordinates are
arguments of the operation. Each added point is displayed in the ObjectEditor graphics window.
As before, we would like to create an object responsible only for creating and manipulating the shapes,
leaving the details of displaying and editing the shapes to ObjectEditor. To create a point on the screen, we
can use the ObjectEditor class, PointModel, which, as we saw before, represents a point that is
automatically displayed by ObjectEditor as a small, filled circle. In this example, we must create a dynamic
number of points. We have seen in Chapter 12 how to create a dynamic collection of textual elements. We
can create a dynamic number of graphics elements in much the same way.
Let us, then, derive the interface of the object being edited in Figure 5. From the left window of Figure 5,
we can directly derive one of the methods of this interface:
public void addElement (int x, int y);
If this object is not going to actually display the points, it must expose them to another object, in this case
ObjectEditor, that performs this function. As we discussed in Chapter 12, it cannot use getter and setter
methods to expose these elements, since they work only for objects with a static number of elements. To
identify how a dynamic list of elements can be exposed, consider the StringHistory interface we
defined to expose a dynamic collection of strings:
interface StringHistory {
public void addElement(String element);
public String elementAt (int index);
public int size();
}
The methods elementAt() and size(), together, allow an external object to determine all the strings
in the history. We can define the exact same method headers for this example, with the only difference that
instead of strings we would expose values of type PointModel. Since this generalizes to indexable
collections of arbitrary types of elements, ObjectEditor looks in an object for (a) a method named
elementAt() taking a single int parameter, and (b) a parameter-less method named size() to
determine if the object encapsulates an indexable dynamic collection; and uses these methods to access the
elements of the collection. This is in the spirit of looking for methods whose names start with get to
12
determine and access the static properties of an object. In fact, we will refer to methods such as
elementAt() and size() that retrieve dynamic components of a collection as getter methods, and
methods such as addElement() that change the dynamic components of a collection as setter methods.
Even though their names do not begin with get and set, they do perform the task of getting and setting
components of an object.
Thus, our interface becomes:
import shapes.PointModel;
public interface PointHistory {
public void addElement (int x, int y);
public PointModel elementAt (int index);
public int size();
}
We have named the interface PointHistory to because, like StringHistory, it also defines a
history, allowing elements to be appended but not inserted in the middle or deleted. After implementing
StringHistory, these three methods do not present any new implementation challenges. Instead of
repeating the code we used in StringHistory, however, we will simply use Java vectors, which make
the implementations of these methods trivial.
Vectors
Java vectors are instances of the class, java.util.Vector, which, like StringHistory and
StringDatabase, defines variable-size collections.
It defines the method:
public final Object elementAt(int index)
for returning the element at the specified index. The final keyword simply says that the method cannot
be overridden in subclasses. Note that the type of an element is Object. This means that we can store
arbitrary objects in a vector. It provides the method:
public final void setElementAt(Object obj, int index)
to set an element at a particular index. The constructor:
public Vector()
creates an empty vector. We can dynamically add elements to a vector using :
public final void addElement(Object obj)
Like addElement() in StringHistory, this method appends a new object to the end of the array.
We can also insert an element in the middle of the array using:
public final void insertElementAt(Object obj, int index)
Once we have added an element, we may want to delete it. If we know its index, we can call:
public final void removeElementAt(int index)
This method removes the element at the specified index. If we know the element to remove, we can call:
public final boolean removeElement(Object obj)
This method is more complicated because the object may not exist or may have been added multiple times.
It removes the first occurrence of the object, and returns false is there was no occurrence.
Like StringDatabase, a vector provides a method determine the index of the first occurrenceof an
object:
public final int indexOf(Object obj)
Finally, if we want to scan each element of the vector in succession, we can call:
public final Enumeration elements()
It returns an instance of the Java interface, java.util.Enumeration, representing the scanned
elements. This interface defines the following methods:
public boolean hasMoreElements();
public Object nextElement();
13
Thus interface is much like the enumeration interfaces we have seen before such as CharEnumeration
except that it enumerates arbitrary objects. As a result, it is appropriate for enumerating the elements of a
vector, which, as mentioned above, can be arbitrary objects.
There are several other methods defined by the class, but these will more than suffice for this course.
The use of some of these methods is illustrated in the implementation of PointHistory given in Figure
6:
import java.util.Vector;
public class APointHistory implements PointHistory {
Vector contents = new Vector();
public void addElement (int x, int y) {
contents.addElement(new PointModel(x, y));
}
public PointModel elementAt (int index) {
return (PointModel) contents.elementAt(index);
}
public int size() {
return contents.size();
}
}
Figure 7 Implementing a History using a Vector
The class defines a vector variable, contents, for storing the points input by the user. The constructor
initializes the variable to an empty vector. The elementAt(),addElement, and size() methods
defined by this class call the corresponding methods on contents. The only tricky part has to do with the
fact that elements of PointHistory are of type PointModel while elements of a vector are of type
Object. Since the former is a subtype of the latter, the statement:
contents.addElement(new PointModel(x, y));
is allowed because the type of the actual parameter, PointModel, is more specific than the expected
type, Object. On the other hand, the statement:
return contents.elementAt(index);
is not allowed, since the type of the actual return value, Object, is more general than the expected type. 5
However, we do know that every element in a vector is a PointModel - the only way an element can get
in the vector is by calling the addElement()method of PointHistory, which takes arguments of
type PointHistory . Thus, we can safely cast the return value to the expected type, PointModel:
return (PointModel) contents.elementAt(index);
It would have been nice if it was possible to restrict the elements of a vector to a type specified by us, much
as we can do for an array – however, that requires special language support not provided to programmerdefined types. Unlike an array, a vector is not part of the standard language but is provided by a library.
Thus, we will often have to cast the types of values extracted from a vector based on the types of the values
we put into it.
In the above code, addElement(), adds a new instance of PointModel each time. What if, in the
interest of saving space, we created a single PointModel:
PointModel pointModel = new PointModel();
and made addElement() add this object always, after changing its position to the location of the new
point:
public void addElement (int x, int y) {
pointModel.setX(x);
pointModel.setY(y);
5
If these rules are still confusing, recall that we are happy to get a car that is different from the one we
expected, as long as it has more features, and a subtype has more methods than its super types
14
contents.addElement(pointModel);
}
While this code is more efficient in that it creates a single PointModel, it does not work. The reason is
that we end up adding a single point multiple times in the history, rather than multiple points! Just because
we added a point at a different location in a vector does not mean Java created a new copy of it.
You might be tempted to, instead, write the following code:
public void addElement (int x, int y) {
PointModel newPointModel = pointModel;
newPointModel.setX(x);
newPointModel.setY(y);
contents.addElement(newPointModel);
}
This also does not work. The assignment statement:
newPointModel = pointModel;
does not create a new PointModel; it makes both variables point at the same object. We will study
this issue in more depth later. It was mentioned here to prevent you from making subtle errors.
In summary, the implementation of our second graphics example, like the first one, is not complicated once
we understand how vectors work, how an object exposes its dynamic indexed elements, and that Java does
not automatically copy objects for us.
Animation
The implementation gets trickier when we wish to animate the graphics. Consider an extension to the
example above that provides a method, animate(), to trace the path defined by the points added in the
history. More precisely, we wish to define a new point, colored white to distinguish it from the history
points, which normally rests at some location, say (0, 0). When we call animate(), the point moves to
the location of the first point we added to the history, then the second one, and so on (Figure 6). After
reaching the last point, it returns to its normal resting position.
(a) Animating Point Resting
(b) Animating Point in Motion
Figure 8 Animating the Trajectory defined by PointHistory
To implement this extension, we must first create the animating point. This is not difficult, all we have to
do is define a readonly point using a getter method:
public PointModel getAnimatingPoint();
This point is readonly because the user does not directly manipulate it, only indirectly by calling
animate().The implementation of this method is, of course, straightforward:
15
static final int ANIMATING_POINT_X = 0, ANIMATING_POINT_Y = 0;
PointModel animatingPoint = new PointModel(ANIMATING_POINT_X,
ANIMATING_POINT_Y);
public PointModel getAnimatingPoint() {
return animatingPoint;
}
The named constants define the normal resting position of the point. As their declaration shows, Java
allows us to create and initialize multiple variables of the same type in a single declaration.
Our next task is to color the point white. A Java library class, java.awt.Color, defines a wide range of
colors, and all shapes (PointModel, RectangleModel, etc.) define a method, setColor(), to
assign them one of these colors. Thus, we can write the following constructor to initialize the color of the
animating point:
static final Color ANIMATING_POINT_COLOR = Color.white;
public APointHistory() {
animatingPoint.setColor(ANIMATING_POINT_COLOR);
}
Now we come to the tricky part, the implementation of the method animate().
In this method, we must visit each point in the history in succession, and move the animating point to the
location of the visited point. There are two ways to visit the history of points – use the vector method
elementAt() to index contents or use the method elements()to enumerate contents,
Because we are not accessing the elements at random indices, but instead, visiting them from the first to the
last, let us enumerate the elements:
public void animate () {
Enumeration points = contents.elements();
while (points.hasMoreElements()) {
animatingPoint.setCenter(((PointModel) points.nextElement()).getCenter());
}
animatingPoint.setX(ANIMATING_POINT_X);
animatingPoint.setY(ANIMATING_POINT_Y);
}
As in the first example, we are using the methods getCenter() and setCenter(), defined on all
shapes, to move the shape. Once the animating point has visited all the points in the history, it returns to its
resting position.
Pausing Execution
If we run animate(), it will not seem to do its job. The reason is that it executes the loop so quickly that
the animating point will return to its resting position before we have noticed it has left, and thus not appear
to move at all. In order to make its effects visible to the human eye; it must give us a chance to observe
each of the locations it visited. The way to do this is to pause execution for some time after it visits each
point. Java provides us with way to make a method pause or “sleep” for a period specified by us, as shown
in the rewritten animation code shown in Figure 8:
static final int ANIMATION_DELAY = 1000;
public synchronized void animate () {
Enumeration points = contents.elements();
while (points.hasMoreElements()) {
animatingPoint.setCenter(((PointModel) points.nextElement()).getCenter());
try {
Thread.sleep(ANIMATION_DELAY);
} catch (InterruptedException e) {
System.out.println("Animation interrupted");
}
16
}
animatingPoint.setX(ANIMATING_POINT_X);
animatingPoint.setY(ANIMATING_POINT_Y);
}
Figure 9 Animating the Trajectory
The method, Thread.sleep(), makes animate() sleep for the number of milliseconds specified by
its argument. We have specified a pause time of one second. There is nothing special about this interval – it
could be smaller or larger – and it is best to experiment with different values of it before settling on one, or
allow the user to set its value, as shown below. If we are interested in fixing the point velocity, then it
depends on the distances between successive history points; and if we are interested in fixing the time to
complete the animation, then it depends on the size of the history.
A sleeping method may be woken up not only when its regular alarm goes off, but also because of some
unexpected condition such as the user terminating the program. To signal an abnormal waking up, the
method throws an InterruptedException. We have therefore enclosed the call to it in a try-catch
block.
Concurrency and Synchronization
Notice the keywords, synchronized, in the header of animate():
public synchronized void animate ()
The reason for this keyword is subtle. For the animation to work, two methods must be active concurrently,
the animate() method and a method called paint() in ObjectEditor that actually draws the animating
point on the screen. If the two methods were executed serially, one after the other, then the drawing method
would be executed before or after animate() is called, when the animating point is always at its resting
position. Thus, it would never show any of the positions the point takes while animate() is executing!
We will study later how we can start these methods concurrently in Java – for now we do not have to worry
how this is done since ObjectEditor takes care of it for us, it being the object that calls both methods.
When two methods are active concurrently, they can step on each other’s toes, leaving data they share in an
inconsistent state. To understand the kind of problems that might occur, imagine two users trying
concurrently editing a program without synchronizing with each other - they can change instance variables
and methods independently, leaving the program in an inconsistent state! The keyword synchronized
before a method tells Java to ensure that it does not interfere with concurrently executing methods. How
Java ensures non-interference is beyond the scope of this course, and will be covered in-depth in your
operating systems course.
If we omit the keyword in a method declaration, ObjectEditor will not execute the method concurrently
with other methods so that concurrency inconsistencies do not occur. We did not put this keyword in other
methods because we were content with seeing their effects on the display after they finished execution.
Thus, without this keyword, animate(), will not appear to work and will essentially have the same
effect on the display as the previous version of the method, seemingly not moving the point. The only
difference would be there would be long pause, equal to the sum of all the sleeps in the method, during
which no other method can be invoked.
As it turns out, this keyword cannot be used in a method declared in an interface. Thus, in the interface,
PointHistory, we must declare the header of animate()as:
public void animate()
even though, in the implementation of the interface, we declare it as:
public synchronized void animate ()
Normally, Java requires matching of all components of corresponding method headers in interfaces and the
classes implementing it, but not in this case, probably to give the implementers of an interface the
flexibility of deciding if they want to allow for concurrency and pay the cost of synchronization.
17
Summary











Each window is associated with a coordinate system in which the origin is the top-left corner the
window, x coordinates increase from left to right, and y coordinates from top to bottom.
Computer-screen coordinates are specified as integer rather than real values because they address
screen pixels, which are finite in number.
A line, rectangle, or oval can be described by specifying its rectangular bounding box.
Components of an object that are instances of certain predefined shape types are displayed graphically
by ObjectEditor as the shapes they denote.
Constraints can be defined among these components, and thus the associated screen images, much as
they are defined among other kinds of components.
A dynamic indexed collection should be made accessible to other objects using methods with standard
names so that the code implementing the collection is easy to understand and the collection elements can
be displayed by a generic tool such as ObjectEditor.
Java provides a built-in type for defining a dynamic, indexed collection of arbitrary objects. Elements
extracted from such a collection may need to be cast to the types of the elements put into the collection.
Java also provides a built-in type for enumerating arbitrary objects, which also may need to be cast to
more specific types.
It is important to make a method pause after it creates the next animation image.
A method that creates the next animation image must execute concurrently with a method that actually
displays the image on the screen.
A method that executes concurrently with one or more other methods should be declared as
synchronized.
Exercises
1.
2.
3.
4.
5.
What is a screen pixel?
Use rectangular bounds to describe a line between the points (50,50) and (25,25).
Explain the convention we studied in this chapter to expose the elements of a dynamic indexed
collection whose elements are of type T.
Explain why a method that creates the next animation image must execute concurrently with a method
that actually displays the image on the screen.
Explain why a cast was needed in the animate() method shown in Figure 8.
6. In this problem, you will write a turtle object, shown in Figure 11 (a) that allows a user to
move a turtle on the screen.
18
(a) Initial State
(b) After Drawing Some Lines
Figure 10 Turtle Graphics
As shown in the figure, the “turtle” is represented by two adjacent, solid, circles, one for the body
and another for the head. The head always points in the direction in which the turtle is pointing.
The turtle can point in the north, south, east or west direction, and can move both forward and
backward in the direction it is pointing. Initially, the turtle is pointing eastwards, as shown in the
figure.
As also shown in the figure, the turtle has three integer properties, StepSize, StepTime, and
MoveDistance, and a boolean property, PenDown. In addition, it provides the following
methods:
1. rotate(): Rotate the turtle 90 degrees in the anti-clockwise direction.
2. move(): Move the turtle the number of pixel units given by the MoveDistance property
. Movement is in forward (backward) direction if argument is positive (negative).
3. animationMove(): Like move(), this operation moves the turtle the number of pixel units
given by the MoveDistance property. While move() essentially makes the turtle take one
19
big step to its new position, this makes it take several small steps to its destination, animating
the turtle as it moves. The size of the step, in pixels, is given by StepSize and the time to
take a step is given by the property, StepTime.
You are free to choose the sizes of the body and head, the size of the drawing window, and the
initial position of the turtle.
Once you have implemented these basic operations, allow users to draw lines by moving the
turtle on the screen, telling the turtle to pick up or put down a pen as it moves (Figure 11(b)). The
boolean property PenDown determines if the pen is up or down.
Finally, implement an animation operation, spin(), that continuously rotates the turtle until the
stop() operation is called. The operation should call rotate() once (that is rotate by 90
degrees) every step time.
20
21
Download