This chapter starts with an overview of AWT (the Abstract Window Toolkit) and its general features and then details the basic structure of AWT and its most important classes. A discussion of more advanced aspects of exception handling follows. AWT and exception handling will be important in developing the first version of the spreadsheet applet, discussed in detail in the last section of this chapter. The visual and user-interface features of the applet will rely on AWT. The underlying spreadsheet engine will make active use of exception handling, particularly when validating and calculating formulas.
AWT forms the basis for graphical user-interface programming in Java. The AWT package offers a large variety of tools for creating graphic widgets such as buttons, list boxes, and scrollbars. A graphics class can be used for two-dimensional drawing operations, such as displaying polygons, painting text, and setting fonts and colors, and graphical operations, such as clipping and scaling. Beyond all this, AWT provides an underlying foundation for interfacing with the user. A series of methods handle events produced by the user and the system, such as mouse clicks and keystrokes. In short, AWT gives you a set of tools for writing simple applets and a basis for developing classes that can be used to create more sophisticated programs. Chapters 4, "Enhancing the Spreadsheet Applet,"and 5, "Adding Graphs and Scrollbars to the Spreadsheet," will have the other parts of the AWT tutorial.
Figure 3.1 illustrates the hierarchy of the most important classes in the AWT package; they are used for a variety of services:
Figure 3.1 : Hierarchy of significant AWT classes.
Most of these classes are discussed and illustrated in more detail throughout this and subsequent chapters.
Component classes are used for coordinating all aspects of a visual
control. A variety of Component methods can be used to process
events, enable or disable a control, set fonts and colors, and
manage the control's visual display. The most widely used Components
will be those simple ones that are part and parcel of creating
a user interface. These include labels, buttons, list boxes, and
choice menus. However, more sophisticated Component subclasses
can be used to manage these primitive controls. The following
list shows the primitive Component classes; these simpler classes
are derived directly from Component.
Primitive Component Classes |
Button |
The Component hierarchy complements these primitive controls with classes based on the Container class. Containers are used to hold Component classes and other Containers. Panels, Windows, Dialogs, and Applets are all notable Container subclasses. The Container classes are presented in more detail shortly.
You can use the primitive controls in Table 3.1 to quickly produce a functional applet. Figure 3.2 shows such an applet; with five components, it enables the user to choose what to display and when to display it. A choice between painting nothing, a rectangle, or text is offered. The class that does the drawing is a custom subclass of the Canvas Component called DrawCanvas, located in the middle of the applet. At the bottom of the screen is a Panel with Button and Choice objects. The Choice menu object lets the user decide what is to be painted when the Button object is clicked (or when the applet repaints). A TextField object at the top of the screen is used to specify which text will be displayed, if text is used.
Figure 3.2 : Example with five companents
Listing 3.1 provides the code for this example. Although the applet is simple, it illustrates a lot of features. The init() portion of the applet class (called Example1) shows how easy it is to create and add classes to the applet display. The handleEvent() method overrides the default event handler and is used to trap button clicks. The code checks to see whether the action came from its button. If so, it forces the DrawCanvas object to repaint. The user's selection in the Choice box will then be reflected.
The DrawCanvas class inherits the features of Canvas and adds its own custom functions to it. When it is drawn with the paint() method, the class queries the applet about the user's selections and paints accordingly. This paint() method can be issued either when the user clicks the button or when a certain region of the canvas needs to be redrawn. The paint() method is part of all Component classes. It must be overridden, as in the DrawCanvas class, if any custom drawing needs to be done.
The paint() method takes
as its sole parameter an instance of the Graphics class. This
class offers different methods that can be used to set the features
of the area being drawn. For example, the DrawCanvas class uses
the drawString() method to
put some text up on the drawing area. The drawRect()
and fillRect() methods paint
a rectangle that draws a border rectangle and its interior, respectively.
These rectangles are painted a color established by the setColor()
method. Since the paint()
method and the Graphics class are at the basis of drawing AWT
images, you will see more examples of these throughout the rest
of the book.
Listing 3.1. An applet with five components.
import java.awt.*;
import java.lang.*;
import java.io.*;
import java.applet.*;
// This program illustrates a simple applet with a
TextField,
// Panel, Button, Choice menu, and Canvas.
public class Example1 extends Applet {
TextField tf;
DrawCanvas c;
Button drawBtn;
Choice ch;
// Add the Components to the screen...
public void init() {
// Set up display area...
resize(300,200);
setLayout(new BorderLayout());
// Add the components...
// Add the text at the top.
tf = new TextField();
add("North",tf);
// Add the custom Canvas to the center
c = new DrawCanvas(this);
add("Center",c);
// Create the panel with button and choices at the
bottom...
Panel p = new Panel();
drawBtn = new Button("Draw Choice Item");
p.add(drawBtn);
// Create the choice box and add the options...
ch = new Choice();
ch.addItem("Rectangle");
ch.addItem("Empty");
ch.addItem("Text");
p.add(ch);
add("South",p);
}
// Handle events that have occurred
public boolean handleEvent(Event evt) {
switch(evt.id) {
// This can be handled
case Event.ACTION_EVENT: {
if(evt.target instanceof Button) {
// Repaint canvas to use new choices...
c.repaint();
} // end if
return false;
}
default:
return false;
}
}
// Return the current choice to display...
public String getChoice() {
return ch.getSelectedItem();
}
// Return the text in the list box...
public String getTextString() {
return tf.getText();
}
}
// This is a custom canvas that is used for drawing
// text, a rectangle, or nothing...
class DrawCanvas extends Canvas {
Example1 e1app;
// Constructor - store the applet to get drawing
info...
public DrawCanvas(Example1 a) {
e1app = a;
}
// Draw the image per the choices in the applet...
public synchronized void paint (Graphics g) {
// Get the current size of the display area...
Dimension dm = size();
// Draw based on choice...
String s = e1app.getChoice();
// Calculate center coordinates...
int x,y,width,height;
x = dm.width/4;
y = dm.height / 4;
width = dm.width / 2;
height = dm.height / 2;
// Paint a rectangle in the center...
if (s.compareTo("Rectangle") == 0) {
// Draw the rectangle in the center with colors!
g.setColor(Color.blue);
g.drawRect(x,y,width,height);
g.setColor(Color.yellow);
g.fillRect(x + 1,y + 1,width - 2,height - 2);
} // end if
// Get the text in the applet and display in the
middle...
if (s.compareTo("Text") == 0) {
String displayText = e1app.getTextString();
g.setColor(Color.red);
g.drawString(displayText,x,y + (height/2));
}
}
}
The other Component in this example is a Panel, which is a subclass of Container. In the example, the Panel contains the Button and Choice objects. Containers are good for managing a group of Components and have a special meaning in AWT regarding how an object is displayed. Containers function as a broker for how a component within it is presented. If a component's display coordinates are outside the region of a container, it is clipped. More importantly, Containers provide a mechanism for how an object is presented, especially its size and position. This is closely tied to how Layouts work, discussed briefly in the following section.
One interesting aspect of AWT is that the Applet class is derived from Panel. This may seem unusual at first; however, an Applet really functions as a Container. Objects that do not fit within its area are clipped in display, and if the Applet is destroyed, so are the objects within it. Given this, the Applet class can be easily understood as simply a Panel with additional functions that tie it to the workings of the native browser.
The Window, Frame, and Dialog classes are also Containers that figure prominently. These are used to create objects that "pop up" outside the space of the applet, giving a multidimensional feel to an otherwise "flat" Web page. These classes will be discussed in more detail in the next chapter.
For someone who has not seen Java applet code before, one question that might immediately come to mind from the previous example is: How does the program know where to position the Component objects on the screen? After all, there is no coordinate information in the code. And what do code expressions like add("North", tf) mean?
The key to understanding the previous code and how AWT displays components in general is a knowledge of layouts. Containers rely on layouts to give you a way to determine the size and position of the components within it. Every container is tied to a single instance of a layout. The layout that a container uses is either set by default or through programming. In Listing 3.1, the layout of the Applet object, Example1, is set to an instance of the BorderLayout class by the line:
setLayout(new BorderLayout());
Since the Panel class is also a container, it too has a layout. The default class, FlowLayout, is used in the example.
Layouts are an important part of AWT because they take care of a major issue in Java: portability. Although the language portion of Java takes care of software portability concerns, it cannot, by nature, take care of visual portability issues. Namely, how can Java guarantee that an applet developed on a specific GUI (such as Windows 95) and on a specific monitor resolution (such as VGA) have a proper look and feel on other platforms? The AWT package was designed to solve this problem. By providing a level of abstraction above the native GUI, it can hide the specifics of the underlying environment from the developer. Layouts are key elements of the visual part of the abstraction mechanism. By taking control of the sizing and positioning of components, layouts free the developer from having to worry about how to write an applet that looks good on the variety of monitor resolutions in the field. Layouts dynamically calculate how to present a component by looking at the native display-coordinate system at runtime; the sizing and positioning of the components is based on this runtime information.
All layouts are derived from the LayoutManager interface, which specifies how a layout needs to function. AWT provides five classes (described in the following sections) that set up the LayoutManager interface. This range of layouts allows the developer to choose the appropriate layout manager for the requirements at hand; if these choices aren't enough, a new class can be written. In fact, Java developers have already created several custom layout classes. Various incarnations of a RelativeLayout class have been created that lets you state where components go in relation to each other. By navigating the Java home pages on the Internet, you might find a layout class that better fits your development needs. Some of the entry points to Java on the Internet are mentioned in this book's Introduction.
In Listing 3.1, the applet specifies adding new components through an unusual scheme that uses such terms as "North" and "South". This scheme reflects how the BorderLayout class works. In the example, BorderLayout is tied to the Example1 applet class when the setLayout() method is invoked. This method indicates that the Example1 object, which is an instance of Container, is tied to the BorderLayout object. When a Container uses BorderLayout, a Component is added through a command of this form:
add(String direction, Component);
Direction is one of the following Strings: "North", "South", "East", "West", and "Center". In short, the BorderLayout class uses a directional scheme to position a component based on one of the five direction strings. A component set to the "North" direction is set to the top of the container, one that is set to "South" is positioned at the bottom, and so forth. The size of the components is determined by other runtime information, such as the size of the container (usually set by the resize() method) and the attributes of the displayed component. The default behavior of BorderLayout gives the component set to the "Center" direction any space not used by the other components. In Listing 3.1, the Example1 applet dimensions are set to 300¥200 pixels. The TextField and Panel objects are relatively small and so are set comfortably at the top and bottom of the applet. The remaining display is then used for the "Center" component, the DrawCanvas object. This object takes up the bulk of the applet display area.
That the BorderLayout class can display up to only five components (corresponding to the five directions) might concern users; however, this is not really a problem. Recall that since containers can hold other containers, this limitation does not really exist. Consequently, an applet can contain Panel objects, which in turn can contain other containers, and so forth.
In the example, the Panel class is used to align the button and choice box on the same row. The Panel class can be used as a kind of "toolbar," displaying components along the top or bottom of the screen. It exemplifies this behavior because its default layout is the FlowLayout class (see the following section).
The FlowLayout class is good for displaying components horizontally across the container. It is the default layout for Panels, which can be set to function like a toolbar or to contain related Components, such as OK and Cancel buttons. In most cases, the FlowLayout class will present components across a single row. However, if the components do not fit on one row, a new row is started.
Figure 3.3 illustrates a modification of Listing 3.1, which adds another Choice menu to the left of the button (whose text has been modified). The canvas uses this new Choice object to pick the color used to paint the object chosen in the other Choice menu. Since the order of when a component is added to a container is important in determining the layout display, the color Choice object is added before the button.
Figure 3.3 : Second version, illustrating FlawLayout.
The following code modifies the init() method in Listing 3.1 to build the Panel object shown in Figure 3.4. It is inserted in the source code after the DrawCanvas object is created, but before the Button is made.
// Create the panel with button and choices at
bottom...
Panel p = new Panel();
// Explicitly set to flow layout...
p.setLayout(new FlowLayout());
// Add color choice to left of button...
colorChoice = new Choice();
colorChoice.addItem("Yellow");
colorChoice.addItem("Red");
colorChoice.addItem("Blue");
p.add(colorChoice);
// Add button...
The code explicitly creates a new FlowLayout object for the Panel to use. This is unnecessary, but it illustrates how a FlowLayout object could be set up for Container objects in general.
To use the color Choice menu in the DrawCanvas code, add a method to the Example1 class to return the color selection:
// Get the color to be displayed....
// Convert the String to a Color object
Color getColor() {
String s = colorChoice.getSelectedItem();
if (s.compareTo("Yellow") == 0)
return Color.yellow;
if (s.compareTo("Red") == 0)
return Color.red;
return Color.blue;
}
The DrawCanvas class modifies its color code to get the color choice from the applet class:
g.setColor(e1app.getColor());
You can specify other features of FlowLayout to customize its appearance. By default, components within a container using FlowLayout are aligned along the center. However, alternative FlowLayout constructors can be used to align the components to the left or the right. Figure 3.4 shows how the Panel in the example would look if it were right-aligned. The following code line is all you need to add to modify how the layout is established:
p.setLayout(new FlowLayout(FlowLayout.RIGHT));
You can specify the number of pixels between the components in a container using FlowLayout in an alternative constructor. This difference in the spacing between the components is known as the gap value. Both the horizontal and vertical gap values can be set in FlowLayout. Most of the LayoutManager classes support setting gaps. The discussion of the next layout, GridLayout, will illustrate how to use gap values.
Figure 3.4 : The FlawLayout panel is right-justified.
The GridLayout class is used to set a matrix of components along a number of rows and columns. Since the size of each row and column is the same, each component in the grid has the same size. Each new component added to the container using GridLayout is positioned to the next index in the grid. If the row is not full, it is added to the next column; if it is, a new row is started and the component is added to the new column.
Figure 3.5 illustrates an applet set to use GridLayout. It is a 5¥5 matrix of Buttons. Listing 3.2 shows the code used to create this applet and illustrates the use of gap values. In this case, a horizontal gap of 10 and a vertical gap of 20 is specified in the constructor.
Listing 3.2. Creating an example using GridLayout.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// Class used for illustrating Grid Layouts...
public class GridLayoutExample extends Applet {
// Set up a matrix of numbers to be displayed in a
grid...
public void init() {
// Set up display area...
resize(300,200);
// Set the layout to a 5 by 5 grid with
// a horizontal gap of 10 and a vertical gap of 20
int rowsAcross = 5;
int rowsDown = 5;
setLayout(new GridLayout(rowsAcross,rowsDown,10,20));
// Fill the grid with buttons filled with numbers...
int matrixSize = rowsAcross *rowsDown;
for (int i = 0; i < matrixSize; ++i) {
// Make a label set to the current number...
// Add it to the grid...
add(new Button(Integer.toString(i)) );
}
}
}
Figure 3.5 : An example using GridLayout.
The code ends with a for loop that adds Buttons to the grid. As the number increases, each Button is added across and down the applet display area. The code that creates the numeric name of the Button is interesting because it illustrates a static method of the type wrapper class, Integer, that can be used to convert an integer to a String without creating a new object.
An alternative constructor enables you to create a GridLayout without horizontal and vertical gaps.
The most complex layout class provided with the Java API is GridBagLayout. While it is superficially similar to the GridLayout class, it differs significantly by not requiring the components in the grid to be the same size. GridBagLayout uses a helper class called GridBagConstraints to specify how the component is displayed in relation to the container's other components. GridBagLayout can guarantee a logical display of components because it replaces the use of hard-code coordinates with a relative structure of how the components should visually interrelate.
Figure 3.6 and Listing 3.3 show an example of using GridBagLayout. As the code illustrates, this class is much more complex than the other layouts. The key to using GridBagLayout is understanding its interaction with the GridBagConstraints helper. To describe it in high-level terms, GridBagLayout and GridBagConstraints use a system of weighting and relative flags to determine how things will be positioned and sized. To see how this works, look at some of the GridBagConstraints variables summarized in Table 3.1. The variables starting with "grid" specify positioning in relation to the other components in the row or column. The GridBagConstraints constant REMAINDER means that the object should be the last item in the row or column. A value of 1, on the other hand, indicates that it should be positioned normally. The weightx and weighty variables determine space distribution of the components in relation to each other. A weight of 1 indicates that the item should be positioned evenly with other items of weight 1. On the other hand, a weight of 0 will give a component a lower priority in sizing. If the weight variables are not set to a nonzero value, the default distribution will be moved toward the center of the container.
Figure 3.6 : An example using GridBagLayout.
Listing 3.3. Creating an example using GridBagLayout.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// Class used for illustrating GridBagLayouts...
public class GridBagLayoutExample extends Applet {
// A complex set of buttons
public void init() {
// Just reuse these over & over...
Button b;
Label l;
// Set up display area...
resize(300,200);
// Create the GridBagLayout and its helper...
GridBagLayout g = new GridBagLayout();
setLayout(g);
GridBagConstraints gbc = new GridBagConstraints();
// *************************************
// Put up a row of three equal size buttons...
// *************************************
// This tells the layout to use the full horizontal
// and vertical height if the display area is not
filled...
gbc.fill = GridBagConstraints.BOTH;
// Distribute horizontal space evenly between buttons
gbc.weightx = 1.0;
// Create and add the three buttons...
b = new Button("Number 1");
g.setConstraints(b,gbc);
add(b);
b = new Button("Number 2");
g.setConstraints(b,gbc);
add(b);
b = new Button("Number 3");
gbc.gridwidth = GridBagConstraints.REMAINDER; // Fill
up the row...
g.setConstraints(b,gbc);
add(b);
// *************************************
// Put up a button, a label, and a button
// that uses the remaining height area...
// *************************************
b = new Button("Number 4");
gbc.gridwidth = 1; // Reset to normal...
gbc.weighty = 1.0; // Force it to use remaining
height...
g.setConstraints(b,gbc);
add(b);
l = new Label("Number 5");
g.setConstraints(l,gbc);
add(l);
b = new Button("Number 6");
gbc.gridwidth = GridBagConstraints.REMAINDER; // Fill
up the row...
g.setConstraints(b,gbc);
add(b);
// *************************************
// Make a normal height button with insets...
// *************************************
gbc.weighty = 0.0; // Normal height;
gbc.gridheight = 1;
gbc.weightx = 0.0; // Use up the row...
gbc.insets.left = 20;
gbc.insets.right = 20;
b = new Button("Number 7");
g.setConstraints(b,gbc);
add(b);
// *************************************
// Finally add a text field across the bottom...
// *************************************
gbc.insets.left = 0; // Reset these...
gbc.insets.right = 0;
TextField t = new TextField("Number 8");
g.setConstraints(t,gbc);
add(t);
}
}
Variables | Description |
gridx, gridy | Specifies the upper-left display of the grid cell. A value of GridBagConstraints.RELATIVE indicates that the component is to be placed just right of or below the component just added before. |
Gridwidth, | Indicates the number of grid cells in its display area. The |
gridheight | GridBagConstraints.REMAINDER value specifies that it is the last cell it is the next to last cell in the row or column.in the row or column. GridBagConstraints.RELATIVE indicates that |
Fill | Indicates what to do if the display area is not filled. If this value is set to GridBagConstraints.BOTH, then it will fill up the display area. |
ipadx, ipady | Used to specify internal padding to add to the component's minimum size. |
Insets | Sets the external padding around the component's display area. |
anchor | A directional scheme to indicate where a component should go if it does not fill up the display area. The GridBagConstraints.CENTER variable is the default value. |
weightx, | Determines space distribution. The default value of zero results in |
weighty | the components clumping together in the middle of the display. Otherwise, the value indicates a weighting in relation to the other row and column components. |
Look at the Java API documentation for more information about these variables.
The spreadsheet project throughout this part of the book uses GridBagLayout. In particular, the frame initialization uses this class to set how the text field, spreadsheet canvas, and scrollbars are positioned in relation to each other. Look at these examples to get more ideas about how GridBagLayout works.
The CardLayout class allows the developer to flip through a series of displays. The flipping action of CardLayout is similar to HyperCard or other card-based programs. This card style of presentation differentiates CardLayout from the other layouts in that only one card of information is displayed at a time.
The typical way to use CardLayout is to tie it to a container, like a Panel. A series of cards can then be added to the container. Every time a component is added to a container that uses CardLayout, a new "card" is added. A variety of methods can be used to flip through the cards. For example, the first() method goes to the first card in the deck, next() goes to the next card, show() goes to a card with a certain name, and so on.
Listing 3.4 provides the source code for a program that can be used to flip through a deck displaying different graphical images. It is similar to the first example in this chapter.
Listing 3.4. Creating an example using CardLayout.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This class illustrates card layouts by drawing a
different
// shape for a variety of cards...
public class CardLayoutExample extends
java.applet.Applet {
// Each card consists of a canvas that draws the name
// of the card...
Panel p;
CardLayout panelLayout;
int index = 0;
int lastIndex;
public void init() {
String name;
// Set up display area...
resize(300,200);
setLayout(new BorderLayout());
// Create the panel that uses CardLayout
p = new Panel();
panelLayout = new CardLayout();
p.setLayout(panelLayout);
add("Center",p);
// Add a canvas to each card
// The name variable is the shape to display...
CardLayoutDrawCanvas c; // Reuse these...
// Add the rectangle...
name = "Rectangle";
c = new CardLayoutDrawCanvas(name);
p.add(name,c);
// Add the oval display...
name = "Oval";
c = new CardLayoutDrawCanvas(name);
p.add(name,c);
// Add the round rectangle display...
name = "RoundRect";
c = new CardLayoutDrawCanvas(name);
p.add(name,c);
// Show the first card...
index = 0;
lastIndex = 2;
panelLayout.first(p);
}
// A mouse click takes you to the next card...
public boolean mouseDown(Event ev, int x, int y) {
// Go to the next card or to beginning
if (index != lastIndex) {
panelLayout.next(p);
++index;
}
else { // Go to first card...
panelLayout.first(p);
index = 0;
}
return true;
}
}
// This is a custom canvas that is used for drawing
// text, a rectangle, or nothing...
class CardLayoutDrawCanvas extends Canvas {
String name;
// Constructor - store the applet to get drawing
info...
public CardLayoutDrawCanvas(String s) {
name = s;
}
// Draw the image per the choices in the applet...
public synchronized void paint (Graphics g) {
// Get the current size of the display area...
Dimension dm = size();
// Draw based on choice...
// Calculate center coordinates....
int x,y,width,height;
x = dm.width/4;
y = dm.height / 4;
width = dm.width / 2;
height = dm.height / 2;
// Paint a rectangle in the center...
if (name.compareTo("Rectangle") == 0) {
// Draw the rectangle in the center with colors!
g.setColor(Color.blue);
g.drawRect(x,y,width,height);
g.setColor(Color.yellow);
g.fillRect(x + 1,y + 1,width - 2,height - 2);
} // end if
// Paint an oval in the center...
if (name.compareTo("Oval") == 0) {
// Draw the rectangle in the center with colors!
g.setColor(Color.blue);
g.drawOval(x,y,width,height);
g.setColor(Color.yellow);
g.fillOval(x + 1,y + 1,width - 2,height - 2);
} // end if
if (name.compareTo("RoundRect") == 0) {
// Draw the rectangle in the center with colors!
int rounding = dm.width / 8;
g.setColor(Color.blue);
g.drawRoundRect(x,y,width,height,rounding,rounding);
g.setColor(Color.yellow);
g.fillRoundRect(x + 1,y + 1,width - 2,height - 2,
rounding,rounding);
} // end if
}
}
Programs written for most GUI environments take actions based on events initiated by the user or the system. If the user clicks the mouse, a "mouse click" event is issued. If the program wants to handle the mouse click, it needs to insert some code to trap for any mouse click event. The program may pass the event on to a default handler if it doesn't want to handle the event. The default handler encapsulates an object's standard behavior. For example, when you click a button, it should reflect the action by showing a pressing motion. This default behavior should occur regardless of whether the program processes the mouse click event.
As stated earlier, the visual controls the user interacts with in the AWT environment are derived from the Component class. A critical method of this class is handleEvent(), which is used to process incoming events and relay them to the appropriate handler methods. Any component that needs to manage specific events will need to override this method with its own handler.
In this chapter's initial example, the canvas object that draws shapes and text was repainted every time the user clicked the Draw button. (See Figure 3.3 and Listing 3.1.) The program made this happen by adding the following code to the applet class, Example1:
// Handle events that have occurred
public boolean handleEvent(Event evt) {
switch(evt.id) {
// This can be handled
case Event.ACTION_EVENT: {
if(evt.target instanceof Button) {
// Repaint canvas to use new choices...
c.repaint();
} // end if
return false;
}
default:
return false;
}
}
This code overrides the default handler of the Applet class (which is a subclass of Component). The sample applet would do very little if this code had not been added. Even though the default handler was overridden, the code can still allow default behavior to occur. The return code from the method tells the parent of the component what should happen next. If the method returns true, then the event has been completely handled and should not be passed to the parent. On the other hand, a false return value allows the event to be passed on. The event handler of the component's superclass can also be called through this expression:
return super.handleEvent(evt);
The handleEvent() method takes as its sole parameter an instance of the Event class. This class encapsulates information about the event that occurred. The id integer variable of the class represents the type of event that occurred. The most widely captured event is the one with the id ACTION_EVENT. Each class of component has a specific action tied to it. For example, the action for a Button object is its selection, such as a mouse click. For TextField objects, the action is the entry of the Return key in the text field.
Other types of frequently caught events are those prefixed by KEY_ and MOUSE_, which represent keyboard and mouse events, respectively. Other events include scrollbar actions and window events, such as Minimize or Destroy.
In the preceding code, the handleEvent() method traps for button selections. When the button is selected, an event with an ACTION_EVENT ID is generated. Recall, however, that this example also had a TextField. To differentiate the button selection from a text field Return keystroke, the code needs to tell what class of object issued the event. It does this by looking at the Event target variable. In the example's code, the program checks to see whether the target is a button by using the instanceOf operator.
Other information can be found in Event variables. An optional argument, the arg variable, provides information specific to the Event, such as the Object of an action. For mouse events, the x and y variables can be used to get the mouse position. The key variable is used to determine which keystroke corresponds to a KEY_ event.
The Component class also has helper methods that can be used if the user wants to manage events in a simpler manner than handleEvent() provides. These helper methods are actually called by handleEvent(). However, the overriding of handleEvent() is not required to use the helper methods.
In the CardLayout example, cards were flipped by overriding the mouseDown() method, which was declared as follows.
public boolean mouseDown(Event ev, int x, int y)
This is used to trap mouse clicks, passing the current location of the mouse in the x and y parameters. This call actually begins in the handleEvent() method, which traps for events of ID MOUSE_DOWN. When such events are issued, handleEvent() reacts by calling mouseDown(). If this is overridden by the object in question, its version of the method will be called. Like handleEvent(), the return value of the helper methods indicate whether the event has been completely handled.
Table 3.2 lists the available Event helper methods, which are
all part of the definition of the Component class.
Method | Description |
mouseEnter | Mouse enters the Component's area |
mouseExit | Mouse leaves the Component's area |
mouseMove | Mouse has moved |
mouseDown | Mouse has been pressed down |
mouseDrag | Mouse has moved while it is pressed down |
mouseUp | Mouse click has been released |
keyDown | Keyboard character has been pressed |
keyUp | Keyboard character has been released |
action | An action has occurred to the Component |
gotFocus | The input focus has been placed on the Component |
lostFocus | The Component has lost input focus |
Although the basics of exception handling were discussed in the first part of the book, there is a lot more to managing exceptions in Java than just the "try-catch" clause. The class of exceptions that is thrown and what information can be gleaned from the thrown object are also important topics. This part of exception handling is related to Java's exception class hierarchy, the subject of this section.
All throwable objects in Java are an instance of, or are subclassed from, the Throwable class, which encapsulates the behaviors found in all the throwable classes that are part of the Java API. One widely used method of Throwable is getMethod(). This prints out a detail message attached to the thrown object. The detail message will give extra information regarding the nature of the error. If the default constructor of the thrown object is used, a system-generated detail message will be provided. If you want a custom message, on the other hand, an alternative constructor can be used.
A couple of examples will illustrate using detail messages. Suppose an operation is performed that results in an error. The following code will catch the thrown object and print out the detail message:
try {
// do something that causes an exception, such as
divide by zero
}
catch (Throwable t) {
// Print out the detail message of the error
System.out.println(t.getMessage());
}
In another case, suppose that the same code wants to rethrow the message with its own custom detail message if there is an error. The other constructor for Throwable can be used in this situation to construct the custom message:
try {
// do something that causes an exception, such as
divide by zero
}
catch (Throwable t) {
// Throw a new Throwable object with
// Custom detail message
throw new Throwable("This method threw an
exception");
}
When the new Throwable is caught and getMessage() is invoked, the program will get the String "This method threw an exception" instead of a system-generated message.
Other methods in Throwable can be used for getting the state of the runtime stack when the error occurred. The printStackTrace() method, for example, prints to standard error the kind of stack output that you often see when a Java program terminates abnormally.
All the other exception classes in the Java API behave similarly
to the Throwable class. They differ only in how they are located
in the hierarchy of class exceptions. Java divides errors into
two general groupings, indicated by two parent classes derived
from Throwable. The Exception class represents the kind of errors
that occur in a normal programming environment, such as file not
found, array index out of bounds, null pointers, divide by zero,
and so forth. These are "soft" errors, the type of difficulty
that a program should be able to easily recover from. In general,
Exception classes represent errors that are meant to be caught
by the calling method whenever they occur. Classes derived from
the Error class, on the other hand, represent more serious errors
that can occur at unpredictable times and are often fatal in nature.
An extreme case of such an Error is a failure within the Java
virtual machine. If this occurs, often the best you can hope for
is an orderly shutdown of the program. Because of their unpredictable
and catastrophic nature, Error objects do not have to be caught
in exception handlers. In general, programs will be written to
catch only classes derived from the Exception class.
Note |
From a terminological standpoint, "exception" (lowercase) is used to refer to all classes of thrown objects. The term "error" (lowercase also) refers to the circumstances that cause the object to be thrown. |
As mentioned in the first part of this book, a method establishes that it throws an Exception that has to be caught in its declaration. For example, this is the constructor for the class that opens a file for output:
public FileOutputStream(String name) throws IOException
This declaration means that an instance of IOException is thrown whenever the file specified in the String cannot be opened. The IOException class is derived from Exception, so any code that uses this FileOutputStream constructor must catch this exception. This means that code using this constructor must be generally structured as follows:
try {
FileOutputStream fo = new FileOutputStream("MyFile");
// write the file
{
catch (IOException e) {
// Handle the file open problem
}
With one notable exception, all methods that throw objects of type Exception must be called in an exception handler that catches the Exception. However, Java provides a branch of Exceptions of thrown objects that do not have to be caught. These are derived from the RuntimeException class. Subclasses of the RuntimeException class are those problems that would be too cumbersome to trap every time they may occur. For example, all accesses to arrays could result in an exception because the index into the array could be bad. However, it would be unwieldy to put an exception handler around all array calls. There would also be a performance hit. Even worse are instances of the NullPointerException class being thrown, which theoretically could occur anywhere in the program.
Just because an exception is a subclass of RuntimeException, however, doesn't mean that a good program should not trap for the thrown object. For example, the ArithmeticException usually indicates a divide by zero error. It is good programming to trap for this error whenever it occurs. In most cases, division doesn't often occur in a program. Consequently, a clause like the following is appropriate for many division operations:
try {
result = divider / divisor;
}
catch (ArithmeticException e) {
System.out.println("Divide by zero error!");
result = 0;
}
On the other hand, it is acceptable to have a divide operation
that is not part of an exception handler, since ArithmeticException
is a subclass of RuntimeException. Here is a list of the subclasses
of RuntimeException:
The Subclasses of RuntimeException |
ArithmeticException |
The Exception class hierarchy serves a more fundamental purpose in Java programming than just providing a way to order exception classes; it plays an important role in determining how an exception is handled. When an exception is thrown, Java looks for an exception handler to catch the thrown object. It first looks in the method where the error occurred, checking to see whether it has an appropriate exception handler-one that is the class or a superclass of the exception thrown. Recall that an exception handler can have multiple catch statements. The catch statements should be ordered in such a way that a subclass is listed before any of its superclasses. Here is a possible exception handler for managing a variety of problems:
try {
// some bad arithmetic operation
// or maybe a bad array access
// or a null errror
}
catch (ArithmeticException e) {
// Handle the exception
}
catch (RuntimeException e) {
// Handle the runtime exception
}
catch (Exception e) {
// Handle the Exception
}
catch (Throwable e) {
// Handle the thrown object
}
Recall that ArithmeticException is a subclass of RuntimeException. The latter is derived from Exception, which in turn is a subclass of Throwable. In this example, a divide by zero error throws an ArithmeticException, which is handled in the first catch statement. On the other hand, a NullPointerException or a bad array access will result in a RuntimeException object being thrown, which is handled in the second catch statement. A serious problem of class Error will not be handled until it reaches the last catch statement, which will catch the thrown object since Error is a subclass of Throwable. This example illustrates that the exception class hierarchy is a critical part of Java's strategy for resolving exceptions.
If the method that caused an object to be thrown does not have an appropriate exception handler, the object percolates up the call stack, and this process of finding an appropriate handler is repeated. If it reaches the top of the stack and no appropriate handler is found, the program will terminate abnormally.
It's easy to write your own exception handler. A class is simply created that extends the class that should function as the superclass. If a new IOException handler is needed, for example, it could be written as the following:
public class CustomIOException extends IOException { }
The hard part, however, is deciding what the superclass of the handler should be. In general, it should not be a subclass of Error since these are reserved for "hard" system problems. Using RuntimeException should also be discouraged because of an interesting controversy over whether there should even be such a thing as a RuntimeException class. This is because, in some ways, the use of RuntimeException classes defeats some of the goals of exception handling. By definition, a RuntimeException object does not have to be caught, but this would then increase the likelihood of an exception not being caught at all, forcing the program to terminate abnormally. This defeats a key goal of exception handling, which is to have a graceful resolution of problems. Thus, the use of RuntimeException is reserved for classes of errors that would occur too frequently to have an exception handler every time the pertinent methods are called.
This leaves the subclasses of Exception as the best candidate for being the superclass of new exception classes. Some good examples can be found in the organization of the exceptions in the Java IO package. One of the constructors for the FileInputStream class tries to open up the input file specified in its String parameter. If the file cannot be opened, a FileNotFoundException is thrown. This class is derived from the IOException class, which is based on Exception. Other file error classes are also derived from IOException. Therefore, the IOException class marks a hierarchy for problems related to input/output operations.
Suppose a new set of classes is being created for database operations. It might be helpful to create a new hierarchy of exceptions that correspond to database problems. The class at the top of this hierarchy might be called DatabaseException, which could be derived from Exception since it marks a new branch of the Exception hierarchy. For errors related to problems with the database key, you could create a KeyException class derived from DatabaseException. If the key could not be found, then you could add a KeyNotFoundException, whose superclass is KeyException. These new Exceptions could be declared as follows:
public class DatabaseException extends Exception { }
public class KeyException extends DatabaseException { }
public class KeyNotFoundException extends KeyException { }
In the following example, a method that tries to find a record could then be declared as throwing a KeyNotFoundException if the record is not found:
public findRecord(Key index) throws KeyNotFoundException
The code that invokes this method then can be structured as follows:
try {
db.findRecord(myKey);
}
catch (KeyNotFoundException e) {
// Handle the key not found exception
}
catch (KeyException e) {
// Handle the exception due to a bad key
}
catch (DatabaseException e) {
// Handle any database exception
}
catch (Exception e) {
// Handle the Exception
}
The project in this chapter is a spreadsheet applet that supports rudimentary formula operations and other basic behavior. This version of the project has the following behaviors:
Figure 3.7 shows how the spreadsheet applet appears in a browser. More advanced features, such as scrollbars, dialog boxes, and graphs, will be explored in the upcoming chapters. The version of the spreadsheet currently presented aims to illustrate many of the features of AWT and exception handling discussed in the first section of this chapter.
Figure 3.7 : The first version of the spreadsheet applet.
Table 3.3 explains the classes used in this chapter's version
of the spreadsheet applet.
Table 3.3. Spreadsheet classes.
Class | Description |
Cell | Contains a String and evaluated value corresponding to a single cell. |
CellContainer | Contains a matrix of Cells. The String in each Cell is evaluated according to whether it is a formula, a literal numeric value, or an empty cell. |
FormulaParser | Used to parse out the individual string fragments that make up a single formula. Converts literal strings to their numeric values. |
FormulaParserException | An Exception that is thrown if a formula is ill constructed. |
ArgValue | A FormulaParser helper class used to store information about an argument in a formula. |
SpreadsheetCell | Provides for the visual presentation of a single Cell. |
SpreadsheetContainer | Manages the visual presentation of a matrix of SpreadsheetCells. Provides an interface for changing the value of a cell. Supports the display of a currently highlighted cell. |
SpreadsheetFrame | Provides the main presentation of the spreadsheet by displaying the SpreadsheetContainer, managing mouse selections on that spreadsheet, reading a text field for changing cell values, and handling a simple menu. |
SpreadsheetApplet | Responsible for creating, showing, and hiding the SpreadsheetFrame that provides the visual display of this applet. |
This class stores a String and its evaluated value for a single cell. It performs no validation in terms of the validity of any formulas or nonnumeric values contained in the String. A variable is used to mark whether the cell has been evaluated, although the setting of this variable is of interest only to the classes that use Cell.
Listing 3.5. The Cell class.
// A cell contains the formula for an individual cell
// However, it does not know what to do with it and
simply
// returns its contents...
public class Cell {
String s;
double evaluatedValue;
boolean evaluated; // True if the cell has been
evaluated
// Constructor creates empty StringBuffer as
reference...
public Cell() {
s = "";
evaluatedValue = 0.0;
evaluated = true;
}
// Takes a StringBuffer and makes it the cell's
data...
public void setCellString(StringBuffer s) {
this.s = new String(s);
evaluated = false;
}
// Takes a StringBuffer and makes it the cell's
data...
public void setCellString(String s) {
this.s = s;
evaluated = false;
}
// Return the current contents of the Cell
public String getCellString() {
return s;
}
// Set the evaluated value of a cell...
public void setEvalValue(double val) {
evaluated = true;
evaluatedValue = val;
}
// Set the evaluated value of a cell...
public void setEvalValue(int val) {
setEvalValue((double)val);
}
// See if a cell has been evaluated...
public boolean getEvaluated() {
return evaluated;
}
// Get the evaluated value of a cell...
public double getEvalValue() {
return evaluatedValue;
}
// Set a cell to unevaluated...
public void setEvaluated(boolean eval) {
evaluated = eval;
}
}
This class consists of a matrix of Cells. Its main constructor creates the matrix based on the number of rows and columns provided in its constructor. Public methods allow setting or retrieving values of individual cells based on a row and column index. If this index is bad, an IllegalArgumentException is thrown. The most interesting feature of this class is its ability to evaluate formulas. The recalculateAll() method forces a reevaluation of all the Cell values in the matrix. It works with the FormulaParser class to see how a formula will be calculated. If the formula relies on a Cell that contains yet another formula, the program recurses into finding out the evaluated formula value of that cell. The recursion occurs in the calculateCell() and parseFormula() methods, which determine the numeric value of a specific Cell. The recursion stops when a Cell with a literal (that is, a number) value is found. When a Cell's formula or literal value has been fully evaluated, the Cell is updated accordingly. The calculation process provides a good illustration of exception handling because they are widely used to handle formula parsing errors and illegal formula operations.
Listing 3.6. The CellContainer class.
// The CellContainer class contains a matrix of Cell data.
// The class is responsible for making sure
// that the formulas in the cells are evaluated properly...
public class CellContainer {
int numRows;
int numColumns;
Cell matrix[];
// Constructs an empty container...
public CellContainer() {
numRows = 0;
numColumns = 0;
matrix = null;
}
// Constructs a matrix of cells [rows X columns]
public CellContainer(int rows,int columns) throws IllegalArgumentException {
numRows = rows;
numColumns = columns;
// Throw an exception if the row/col values are no good...
if ((numRows <= 0) || (numColumns <=0)) {
numRows = 0;
numColumns = 0;
matrix = null;
throw new IllegalArgumentException();
}
// Create the Cell matrix...
int numCells = numRows * numColumns;
matrix = new Cell[numCells];
for (int i = 0; i < numCells; ++i)
matrix[i] = new Cell();
}
// Sets the new value of a cell...
public void setCellFormula(StringBuffer s,int row,int col) {
setCellFormula(s.toString(),row,col);
}
// Sets the new value of a cell...
public void setCellFormula(String s,int row,int col) {
// Get the index into the matrix...
int index;
try {
index = getMatrixIndex(row,col);
}
catch (IllegalArgumentException e) {
System.out.println("Invalid CellContainer index.");
return;
}
// Set the value of the cell...
matrix[index].setCellString(s);
}
// Get the string contents of a cell...
public String getCellFormula(int row,int col) throws IllegalArgumentException {
// Get the index into the matrix...
int index;
try {
index = getMatrixIndex(row,col);
}
catch (IllegalArgumentException e) {
throw e;
}
// Good index. Return string...
return matrix[index].getCellString();
}
// Get the cell at certain index...
public Cell getCell(int row,int col) throws IllegalArgumentException {
// Get the index into the matrix...
int index;
try {
index = getMatrixIndex(row,col);
}
catch (IllegalArgumentException e) {
throw e;
}
// Good index. Return Cell...
return matrix[index];
}
// Calculate the matrix index given a row and column...
// Throw an exception if it is bad...
int getMatrixIndex(int row,int col) throws IllegalArgumentException {
// Kick out if there are negative indexes...
if ((row < 0) || (col <0))
throw new IllegalArgumentException();
// Also reject too large indexes...
if ((row >= numRows) || (col >= numColumns))
throw new IllegalArgumentException();
// Everything is OK. Calculate index...
return ((numColumns * row) + col);
}
// Validate a formula by seeing whether it matches the basic syntax...
public String validateFormula(Cell c,String newFormula) throws
ÂFormulaParserException {
// Convert all alphas to Upper Case
String convertedFormula = newFormula.toUpperCase();
// Get old formula of cell and temporarily set cell value there...
String oldFormula = c.getCellString();
// Set up the parser to validate the cell...
FormulaParser f = new FormulaParser(convertedFormula);
// Validate the cell...
try { // Set up the formula parser...
// Get the type of formula it is...
int typeFormula = f.getType();
// If it's empty, return Success...
if (typeFormula == f.EMPTY)
return convertedFormula;
// Check to see whether literal is valid...
if (typeFormula == f.LITERAL) {
f.getLiteral(); // Ignore the return value...
return convertedFormula;
} // end if
// If it's a formula, you need to parse it...
parseFormula(c,f);
}
catch (Exception e) {
throw new FormulaParserException();
}
// Return the converted string...
return convertedFormula;
}
// Recalculate the values in all the cells...
public void recalculateAll() {
if (matrix == null)
return;
// Invalidate the formulas...
invalidateFormulas();
// Go through each cell and calculate its value...
// Go row-wise across, as this is how things are probably set up
int i,j;
for (i = 0; i < numRows; ++i) {
for (j = 0; j < numColumns; ++j) {
if (matrix[(i * numColumns) + j].getEvaluated() == false) {
calculateCell(i,j);
}
} // end column for
} // end row for
}
// Recalculate an individual cell...
// Update its evaluation when complete...
double calculateCell(int row,int col) {
// Get the index of the calculation...
int index;
try {
index = getMatrixIndex(row,col);
}
catch (IllegalArgumentException e) {
return 0.0; // Bad index...
}
// Set up the parser to recalculate the cell...
FormulaParser f = new FormulaParser(matrix[index].getCellString());
// First get the type...
int typeFormula = f.getType();
// If it's empty, you're done...
if (typeFormula == f.EMPTY) {
matrix[index].setEvalValue(0.0);
return 0.0;
}
// If it's a literal, you can also finish quickly...
if (typeFormula == f.LITERAL) {
// It better be some kind of number...
try {
double dbl = f.getLiteral(); // Get the double value...
matrix[index].setEvalValue(dbl);
return dbl;
}
// Some kind of invalid string...
catch(FormulaParserException e) {
System.out.println("Invalid literal at [" + row + "," + col + "]");
matrix[index].setEvalValue(0.0);
return 0.0;
}
}
// Formulas got to be parsed and maybe recurse, however...
double dbl;
try {
dbl = parseFormula(matrix[index],f);
}
catch (Exception e) {
System.out.println("Invalid formula at [ " + row + "," + col + "]");
dbl = 0.0;
}
matrix[index].setEvalValue(dbl);
return dbl;
}
// Parse out a formula...
// Assumes formula parser is set to a certain formula...
double parseFormula(Cell c, FormulaParser f) throws FormulaParserException {
// Figure out what type of formula it is...
try {
int op = f.getOperation();
// Get the arguments...
ArgValue arg1 = new ArgValue();
ArgValue arg2 = new ArgValue();
f.getOpArgs(arg1,arg2);
// Sum operation is different from rest...
if (op != f.SUM) { // SUM is even worse...
double val1,val2;
// Get the values...
// See if you have to recurse...
if (arg1.getType() == arg1.CELL)
val1 = calculateCell(arg1.getRow(),arg1.getColumn());
else
val1 = arg1.getLiteral();
if (arg2.getType() == arg1.CELL)
val2 = calculateCell(arg2.getRow(),arg2.getColumn());
else
val2 = arg2.getLiteral();
// Perform the operation...
switch (op) {
case f.ADD:
return (val1 + val2);
case f.MULT:
return (val1 * val2);
case f.DIV:
try { // Handle divide by zero errors...
double ret = val1 / val2;
return ret;
}
catch (ArithmeticException e) {
// Divide by zero!
return 0.0;
}
case f.SUB:
return (val1 - val2);
default:
break;
} // end switch...
} // end if
else { // Sum...
double dbl = 0.0;
int index;
// Validate row-wise or column operation...
if ((arg1.getType() != arg1.CELL) || (arg2.getType() != arg2.CELL))
throw new FormulaParserException();
// Row-wise or column-wise...
if (arg1.getRow() == arg2.getRow()) {
if (arg2.getColumn() < arg1.getColumn())
throw new FormulaParserException();
for (int i = arg1.getColumn(); i <= arg2.getColumn(); ++i) {
// Skip cases where the cells are the same...
index = getMatrixIndex(arg2.getRow(),i);
if (matrix[index] == c)
continue;
// If OK, then recurse...
dbl += calculateCell(arg2.getRow(),i);
} // end for
return dbl;
}
else if (arg1.getColumn() == arg2.getColumn()) {
if (arg2.getRow() < arg1.getRow())
throw new FormulaParserException();
for (int i = arg1.getRow(); i <= arg2.getRow(); ++i) {
// Skip cases where the cells are the same...
index = getMatrixIndex(i,arg2.getColumn;
if (matrix[index] == c)
continue;
// If OK, then recurse...
dbl += calculateCell(i,arg2.getColumn());
} // end for
return dbl;
}
throw new FormulaParserException();
}
return 0.0;
}
catch (FormulaParserException e) {
throw e;
}
}
// Invalidate all cells that are formulas to force recalculation...
void invalidateFormulas() {
// Set up the parser to get the cell type...
FormulaParser f = new FormulaParser();
int numCells = numRows * numColumns;
for (int i = 0; i < numCells; ++i) {
f.setFormula(matrix[i].getCellString());
if (f.getType() == f.FORMULA)
matrix[i].setEvaluated(false);
} // end for
}
// Get the number of rows in the container
int getNumRows() {
return numRows;
}
// Get the number of columns in the container
int getNumColumns() {
return numColumns;
}
}
This class is used to parse the elements of a single formula. Its public integer variables are used to indicate what kind of operation (such as SUM) is being performed or whether the formula is a literal or empty value. It uses internal hints to keep track of the current parsing operation. It not only returns the type of operation the formula performs, but also the contents of its arguments. For example, the formula SUM(A0,A3) results in the first argument being parsed into row 0 and column 0 (corresponding to A0) and row 0 and column 3 (cell A3). These are stored in the helper ArgValue class. A FormulaParserException object is thrown if there is anything wrong with the formula or literal.
For the sake of saving space, you are referred to the accompanying CD-ROM for the source code of this class.
This exception is thrown when a String does not contain a proper
formula, for any of a variety of reasons. It is a subclass of
IllegalArgumentException.
Listing 3.7. The FormulaParserException class.
// This class is an Exception thrown when a
// formula is an invalid format...
public class FormulaParserException extends IllegalArgumentException { }
This class does little more than hold information about an argument. Public integer variables are used to indicate whether the argument is a literal or cell value. If it is the former, the class stores the converted double value. If it is a cell in a spreadsheet, then its converted row and column values are stored.
In the interest of saving space, see the accompanying CD-ROM for the source code of this class.
This class is used to draw the contents of an individual Cell. It is an extension of the Canvas class and has a custom paint() method that is used to draw the Cell at specific coordinates. It gets the evaluated value of the Cell to determine what to display. The text and background color of the Cell can be set through public methods. It is also possible to indicate that the literal Cell value should be painted and the evaluated value should be ignored.
Listing 3.8. The SpreadsheetCell class.
import java.awt.*;
import java.lang.*;
// This class ties the contents of an individual
// SpreadsheetCell to a single data Cell.
// The evaluated contents of
// that cell are returned to the SpreadsheetContainer...
public class SpreadsheetCell extends Canvas {
Cell c; // The cell this is tied to...
boolean literal; // If set to true, automatically paint what's in
// cell string...
Color fillColor;
Color textColor;
public SpreadsheetCell(Cell c,boolean literal) {
super();
this.c = c;
this.literal = literal;
// Set the color defaults...
fillColor = Color.white;
textColor = Color.black;
}
// Set the fill color
public void setFillColor(Color clr) {
fillColor = clr;
}
// Set the text color
public void setTextColor(Color clr) {
textColor = clr;
}
// Return the reference to the cell...
public Cell getCell() {
return c;
}
// Set the cell string...
public void setString(String s) {
c.setCellString(s);
}
// Get the current string value in the cell...
public String getString() {
return c.getCellString();
}
// This will return the text to the current evaluated contents
// of the cell...
public synchronized void paint(Graphics g,int x,int y,int width,int height) {
String s = c.getCellString();
String textPaint;
// If this is a literal value, then print what it has...
if (literal == true)
textPaint = s;
else {
// Otherwise, display formula only if cell is not empty...
if (s.compareTo("") == 0)
textPaint = s;
else // Otherwise, show evaluate value...
textPaint = String.valueOf(c.getEvalValue()) ;
} // end else
// Set up drawing rectangle...
g.setColor(Color.blue);
g.drawRect(x,y,width,height);
g.setColor(fillColor);
g.fillRect(x + 1,y + 1,width - 2,height - 2);
// Clip the text if necessary...
int textWidth;
int len = textPaint.length();
int effWidth = width - 4;
// Loop until text is small enough to fit...
while (len > 0) {
textWidth = g.getFontMetrics().stringWidth(textPaint);
if (textWidth < effWidth)
break;
-len;
textPaint = textPaint.substring(0,len);
} // end while
// Draw the string
g.setColor(textColor);
g.drawString(textPaint,x + 4,y + (height - 2));
}
// Return the literal value...
public boolean getLiteral() {
return literal;
}
}
This class constructs the spreadsheet to be displayed. It takes as input an instance of CellContainer that contains the matrix being operated on. It constructs a matrix of Spreadsheet cells, each of which is tied to an individual Cell in the CellContainer, except for the headers. These are represented by SpreadsheetCells (set to a literal value) created on the boundaries of the spreadsheet to display the row and column headers.
The SpreadsheetContainer class is derived from the Canvas class. It overrides the paint() method to draw the SpreadsheetCells. It goes across and down the spreadsheet matrix repeatedly calling the SpreadsheetCell's paint() method, providing the coordinates of where it should be drawn. Note the update() method that is called before paint(); this was added to prevent flicker. The default behavior of update() is to blank out the painting area with a white color; this causes a "flicker" until the paint() method is next called. By overriding update() with a direct call to paint(), however, you can avoid the flicker. Try removing the update() method from the SpreadsheetContainer class, and you can see the flicker that results.
The SpreadsheetContainer class also controls the currently highlighted cell by setting the background and text color of the highlighted SpreadsheetCell. It also overrides the handleEvent() method so it can trap mouse clicks. It checks to see whether the mouse is over a valid cell; if so, it is given the highlight.
Listing 3.9. The SpreadsheetContainer class.
import java.awt.*;
import java.lang.*;
// This class contains the cells that make up a spreadsheet...
public class SpreadsheetContainer extends Canvas {
CellContainer c; // The actual spreadsheet data...
int numRows;
int numColumns;
SpreadsheetCell matrix[];
int cellWidth; // These are set in the paint routine...
int cellHeight;
SpreadsheetCell newHighlight;
SpreadsheetCell oldHighlight;
// Construct container. Create internal paint matrix tied to
// the data container...
public SpreadsheetContainer(CellContainer ctnr) {
super();
// Load the container and set up the display...
loadContainer(ctnr);
}
// Take a cell container and load set the spreadsheet
// to use it. Put the highlight in the first cell...
void loadContainer(CellContainer ctnr) {
c = ctnr; // Store the CellContainer...
// Get size of spreadsheet...
numRows = c.getNumRows() + 1;
numColumns = c.getNumColumns() + 1;
// Create the SpreadsheetCell matrix...
matrix = new SpreadsheetCell[numRows * numColumns];
// Add the cells to the grid...
int i,j,index;
char ch;
// Add the column labels across the top...
for (j = 0; j < numColumns; ++j) {
// Create a literal cell for each column...
matrix[j] = new SpreadsheetCell(new Cell(),true);
// Set the cell contents and color...
if (j > 0)
matrix[j].setString(String.valueOf((j - 1)) );
matrix[j].setFillColor(Color.lightGray);
matrix[j].setTextColor(Color.blue);
} // end for
// Create the individual rows...
for (i = 1; i < numRows; ++i) {
// Set up the row header...
index = (i * (numColumns));
matrix[index] = new SpreadsheetCell(new Cell(),true);
ch = (char)('A' + (i - 1));
matrix[index].setString(String.valueOf(ch) );
matrix[index].setFillColor(Color.lightGray);
matrix[index].setTextColor(Color.blue);
// Now set the container cells...
for (j = 1; j < numColumns; ++j) {
index = (i * (numColumns)) + j;
matrix[index] = new SpreadsheetCell(c.getCell(i - 1,j - 1),false);
// Set the colors...
matrix[index].setFillColor(Color.white);
matrix[index].setTextColor(Color.black);
} // end inner for...
} // end outer for
// Highlight the upper-left cell...
index = getIndex(1,1);
newHighlight = matrix[index];
oldHighlight = newHighlight;
setCellHighlight(newHighlight,true);
}
// Attach a new container to the spreadsheet...
public void newCellContainer(CellContainer ctnr) {
// Load the container and set up the display...
loadContainer(ctnr);
repaint();
}
// Return the currently highlighted row...
public SpreadsheetCell getHighlight() {
return newHighlight;
}
// Get the index into the matrix for a row or column...
int getIndex(int row, int col) {
return ((row * numColumns) + col);
}
// Handle mouse clicks...
void setMouseHighlight(int x, int y) {
// First figure out what cell is at those coordinates...
newHighlight = calculatePaint(null,false,x,y);
// Make it the new highlight if it is not a border element...
if ((newHighlight != null) && (newHighlight.getLiteral() == false) ) {
// Turn off old highlight...
if ((oldHighlight != null) && (oldHighlight != newHighlight))
setCellHighlight(oldHighlight,false);
// Set new highlight...
setCellHighlight(newHighlight,true);
oldHighlight = newHighlight;
// Notify parent of change...
notifyParentOfHighlight(newHighlight);
}
}
// Highlight a cell
// If boolean is on then highlight; else set to normal...
void setCellHighlight(SpreadsheetCell sc,boolean on) {
if (on == true) { // Highlight it!
sc.setFillColor(Color.red);
sc.setTextColor(Color.white);
} // end if
else { // Set to normal...
sc.setFillColor(Color.white);
sc.setTextColor(Color.black);
} // end else...
// Force the cell to repaint...
repaint();
}
// Update message sent when repainting is needed...
// Prevent paint from getting cleared out...
public void update(Graphics g) {
paint(g);
}
// Draw the displayable spreadsheet contents...
public synchronized void paint (Graphics g) {
// Go through the calculations of the paint while painting...
calculatePaint(g,true,0,0);
}
// This goes through the motions of calculating what is on the
// screen and either calculates coordinates or paints...
// If it is not paint, returns cell that fits in hit region...
SpreadsheetCell calculatePaint(Graphics g,boolean bPaint,int xHit,int yHit) {
// Get the current size of the display area...
Dimension dm = size();
// Calculate the cell width and height
// Cell should be wide enough to show 8 digits...
if (bPaint == true) {
cellWidth = g.getFontMetrics().stringWidth("12345.67");
cellHeight = g.getFontMetrics().getHeight();
} // end if
// Figure out how many rows and cols can be displayed
int nCol = Math.min(numColumns,dm.width / cellWidth);
int nRow = Math.min((numRows + 1),dm.height / cellHeight);
// Draw the cells...
int index,i,x,j,y;
-nRow;
// Go across the rows...
for (i = 0; i < nRow; ++i) {
y = cellHeight + (i * cellHeight);
// Go across the colomns...
for (j = 0; j < nCol; ++j) {
index = (i * numColumns) + j;
// Paint if told to...
if (bPaint == true) {
matrix[index].paint(g, (j * cellWidth),y,
cellWidth,cellHeight);
} // end if
else { // Otherwise see whether cell fits...
x = (j * cellWidth);
// See whether it fits in the column...
if ((xHit >= x) && (xHit < (x + cellWidth))) {
// See whether it fits in the row...
if ((yHit >= y) && (yHit < (y + cellHeight))) {
return matrix[index];
} // end if
} // end if
}
} // end column for
} // end row for
return null; // Only used if paint is false...
}
// Notify parent that there is a new Highlight...
void notifyParentOfHighlight(SpreadsheetCell sc) {
// Create new event with highlight cell as arg
Event ev = new Event(this,Event.ACTION_EVENT,sc);
// Send it to the parent...
getParent().deliverEvent(ev);
}
// Handle mouse clicks to spreadsheet...
public boolean handleEvent(Event evt) {
switch(evt.id) {
// Mouse clicks. See whether you should highlight
// cell on spreadsheet...
case Event.MOUSE_DOWN: {
if (evt.target instanceof SpreadsheetContainer)
setMouseHighlight(evt.x,evt.y);
return false;
}
default:
return false;
}
}
// Handle to change to a formula...
// Throws an exception if the formula is invalid...
public void replaceFormula(SpreadsheetCell sc,String newFormula) throws
ÂIllegalArgumentException {
String convertedFormula;
// First validate the formula...
try {
convertedFormula = c.validateFormula(sc.getCell(),newFormula);
}
// If formula is invalid, rethrow an exception...
catch (FormulaParserException e) {
throw new IllegalArgumentException();
}
// Add converted formula to cell...
sc.setString(convertedFormula);
// Recalc...
c.recalculateAll();
// Repaint...
repaint();
}
}
The SpreadsheetFrame class is responsible for presenting the spreadsheet applet to the user. Its most important component is the SpreadsheetContainer class, displayed in the middle of the applet. It creates the CellContainer class that is passed to the SpreadsheetContainer. It also has a TextField object used to edit the individual Cell values. The components of SpreadsheetFrame are displayed using the GridBagLayout manager.
When the user clicks on a cell in the SpreadsheetContainer, the SpreadsheetFrame class gets a notification of the current cell highlighted and sticks the text of the cell into the TextField. When the user enters the text, the new formula is validated, and the spreadsheet is redisplayed with the recalculated values.
The SpreadsheetFrame also has a menu attached to it. Currently, the only menu options are Quit and New for a new spreadsheet. Frames and menus will be discussed in more detail in the next chapter.]
Listing 3.10. The SpreadsheetFrame Class.
// THIS CLASS IS NOT PUBLIC! Compile in same file as SpreadsheetApplet class.
// This is the frame that controls that user interaction
// with the applet. It creates the initial spreadsheet data
// and visual container, along with the input field for
// changing values of a cell, the scrollbars, and the menus
class SpreadsheetFrame extends Frame {
CellContainer c; // The actual spreadsheet data...
SpreadsheetContainer s; // The spreadsheet view
Scrollbar hScroll; // The scrollbars...
Scrollbar vScroll;
GridBagLayout g; // Layout for Frame
MenuBar mbar; // The frames menu...
TextField t; // The text field for the spreadsheet...
SpreadsheetCell currHighlight; // The currently highlighted cell...
Applet appl; // The applet...
int numRows; // Keep the initial parameters...
int numCols;
// The constructor for the spreadsheet frame takes the
// values of the size of the Spreadsheet...
public SpreadsheetFrame(Applet a,int rows, int cols) {
super("Spreadsheet Applet");
// Set the initial size and layouts...
resize(300,200);
g = new GridBagLayout();
setLayout(g);
appl = a; // Store the applet...
numRows = rows;
numCols = cols;
// Create the new container based on the applet parameters...
try {
c = new CellContainer(rows,cols);
}
catch (IllegalArgumentException e) {
System.out.println("Invalid Spreadsheet parameters");
dispose();
}
// Add some fake data to see how it works...
addTestData();
// Create display components...
addDisplayComponents();
// Add the menu choices to the frames
addMenuItems();
// Pack before display...
pack();
resize(300,200); // Then reset to default value...
show();
}
// Handle system and user events...
public boolean handleEvent(Event evt) {
switch(evt.id) {
case Event.WINDOW_DESTROY: {
dispose(); // Kill the frame...
return true;
}
// This can be handled
case Event.ACTION_EVENT: {
String menu_name = evt.arg.toString();
if (evt.target instanceof MenuItem) {
// Exit...
if(menu_name.equals("Quit"))
dispose(); // Kill the frame...
// New Spreadsheet...
if(menu_name.equals("New"))
newSpreadsheet();
} // end if
if (evt.target instanceof TextField) {
validateNewFormula();
return true;
} // end if
if (evt.target instanceof SpreadsheetContainer) {
changeInputFormula((SpreadsheetCell)evt.arg);
return true;
}
return false;
}
default:
return false;
}
}
// Add the menu choices to the frames
void addMenuItems() {
mbar = new MenuBar();
Menu m = new Menu("File");
m.add(new MenuItem("New"));
m.addSeparator();
m.add(new MenuItem("Quit"));
mbar.add(m);
setMenuBar(mbar);
}
// Add the spreadsheet and input field
// to display...
void addDisplayComponents() {
GridBagConstraints gbc = new GridBagConstraints();
// Create an input field across the top...
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.gridwidth = GridBagConstraints.REMAINDER;
t = new TextField();
g.setConstraints(t,gbc);
add(t);
// Create the spreadsheet display...
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
gbc.gridwidth = GridBagConstraints.RELATIVE;
gbc.gridheight = 10;
s = new SpreadsheetContainer(c);
g.setConstraints(s,gbc);
add(s);
// Set initial formula for text field...
changeInputFormula(s.getHighlight());
}
// Change the formula of the input field to that of
// the Object argument, which is a spreadsheet cell...
void changeInputFormula(SpreadsheetCell sc) {
// Set the text box with the formula...
if (sc != null)
t.setText(sc.getString());
else
t.setText("");
// Store the currently highlighted cell...
currHighlight = sc;
}
// A text field formula has been entered...
// Validate it and update spreadsheet...
// Update text field where necessary...
void validateNewFormula() {
// Put up wait icon for calculations...
int oldCursor = getCursorType();
setCursor(WAIT_CURSOR);
try {
// Replace the formula. If no problem, then
// spreadsheet will be recalculated...
s.replaceFormula(currHighlight, t.getText());
appl.getAppletContext().showStatus("Formula accepted.");
}
catch (Exception e) { // Handle illegal exception...
// Let browser status bar know about error...
// Get the status bar from the AppletContext...
appl.getAppletContext().showStatus("Illegal Formula syntax:
ÂUse SUM,ADD,SUB,MULT,DIV");
}
// Always place the converted formula in the text field...
changeInputFormula(s.getHighlight());
setCursor(oldCursor);
}
// Reload spreadsheet with blank data...
void newSpreadsheet() {
// Create the new container based on the applet parameters...
try {
c = new CellContainer(numRows,numCols);
s.newCellContainer(c);
// Set initial formula for text field...
changeInputFormula(s.getHighlight());
}
catch (IllegalArgumentException e) {
System.out.println("Invalid Spreadsheet parameters");
dispose();
}
}
// Just some test data...
void addTestData() {
c.setCellFormula("1",0,0);
c.setCellFormula("2",0,1);
c.setCellFormula("3",0,2);
c.setCellFormula("4",0,3);
c.setCellFormula("SUM(A0,A3)",0,4); // Should be 10
// c.setCellFormula("ADD(A0,A3)",0,4); // Should be 5
// c.setCellFormula("ADD(A0,4)",0,4); // Should be 5
// c.setCellFormula("ADD(1,4)",0,4); // Should be 5
c.recalculateAll();
}
}
This class takes the applet parameters and determines the size of the spreadsheet to be constructed. If the parameters are bad, default values are used. When row and column sizes of the spreadsheet are determined, an instance of the SpreadsheetFrame class is created and the visual portion of the program begins. When the user moves away from the current Web page, the spreadsheet is hidden; it is redisplayed if the user returns.
Listing 3.11. The SpreadsheetApplet class.
// This file describes the applet class that manages the
// spreadsheet program.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This applet kicks off the SpreadsheetFrame
// that manages the spreadsheet program.
public class SpreadsheetApplet extends Applet {
SpreadsheetFrame fr;
public void init() {
int rows = 10; // Default if params are no good...
int cols = 10;
// Get the HTML parameters
// and try to convert to a good value...
// Get the row...
try {
String param = getParameter("rows");
int temp = Integer.parseInt(param);
if ((temp > 1) && (temp < 26))
rows = temp;
else
throw new IllegalArgumentException();
}
catch (Exception e) { // Display error to browser...
getAppletContext().showStatus("Invalid row parameter. Using default...");
}
// Get the column...
try {
String param = getParameter("columns");
int temp = Integer.parseInt(param);
if ((temp > 1) && (temp < 40))
cols = temp;
else
throw new IllegalArgumentException();
}
catch (Exception e) {
getAppletContext().showStatus("Invalid column parameter. Using default...");
}
// Create the spreadsheet frame
fr = new SpreadsheetFrame(this,rows,cols);
}
// If returning to screen, show frame...
public void start() {
// Redisplay the applet...
try {
fr.show();
}
// Handle any problem...
catch(Exception e) { }
}
// Hide the frame upon leaving...
public void stop() {
// Handle where it may have been disposed...
try {
fr.hide();
}
// Handle any problem...
catch(Exception e) { }
}
}
In this chapter, you have read about many of the basics of AWT and seen it applied to the creation of a non-trivial spreadsheet. In Chapter 4, "Enhancing the Spreadsheet Applet," you will see how dialog boxes, streams, colors, and fonts can be applied to improve the spreadsheet applet. More of the fundamentals of AWT will be discussed in the context of these enhancements. You will also see a practical example of streams as the ability to save and open a spreadsheet is added to the applet.