All Categories :
Java
Chapter 4
Enhancing the Spreadsheet Applet
CONTENTS
The previous chapter laid the foundations
for the spreadsheet applet to be developed throughout this part
of the book. In creating this first version of the spreadsheet,
Chapter 3, "Building a Spreadsheet
Applet," introduces many of the fundamental concepts of AWT
programming. It also discusses exception handling and practical
ways to handle errors in a Java application. However, this first
version of the spreadsheet applet also includes elements such
as menus and frames that were not discussed at the time they were
presented. These concepts are expanded on in this chapter.
While exploring these aspects of Java programming, this chapter
will illustrate more features of the AWT package, and discussing
the Dialog classes will lead to the practical addition of dialog
boxes to the spreadsheet applet. Custom dialog boxes will be created
for the purposes of selecting the color and font of a spreadsheet
cell; they will be accessed through an expanded menu and their
selections will be made through upgraded classes. This chapter
also includes an introduction to streams programming, which is
used to add the capability of saving and reading a spreadsheet
file. Security issues related to file storage in Java will be
an important part of this discussion.
The previous chapter's version of the spreadsheet applet did not
cover how to use the Frame and Menu classes. These features of
AWT are actually closely related. The Frame class also has a common
relationship with the Dialog class because they both are from
the same superclass, Window; therefore, dialog boxes will also
be covered in this section.
AWT applications can also be enhanced through the support of multiple
colors and fonts. Using them can not only make an applet more
visually pleasing, but can also give the user more freedom in
customizing the spreadsheet's display. The Color, Font, and FontMetrics
classes that make this possible are, therefore, also an appropriate
topic of discussion here.
The Window class is used in AWT to create "popup" windows
that appear outside the constraints of the normal browser area
allocated to an applet. The Window class is derived from the Container
class and so can contain other components. Unlike applet components
tied directly to a browser page, Window classes are not restricted
to a prespecified area of the screen. Window objects can be resized
as their immediate requirements dictate. AWT can perform this
automatically through the Window class's pack()
method; it works with the Window layout (by default, BorderLayout)
to arrive at the optimal presentation of the window, given its
contained components and screen resolution. Typically, pack()
is called before a window is displayed. Windows are not made visible
until the show() method is
called. They are removed from the screen, and their resources
freed, when the dispose()
method is invoked.
The Frame class extends Window by adding a title bar, a border
for resizing, support for menus, and the ability to modify the
system cursor to various states such as waiting or moving. For
most GUI platforms, the Frame's title bar will be tied to system
control boxes, such as minimize, maximize, or destroy. Consequently,
the Frame class has all the elements necessary to make an applet
look like a "real" application, complete with menus
and system controls.
Figure 4.1 and Listing 4.1 present a simple Frame applet that
changes the cursor to a state based on the button selected. The
applet class, FrameCursorApplet, does little more than launch
the main frame, FrameCursor. The constructor for this frame begins
by calling the frame super constructor. The sole parameter in
this case is the caption displayed on the title bar. The layout
for the Frame is then set. The default is BorderLayout, but in
this case, you want a three by two grid matrix, hence the use
of GridLayout. The buttons are added to the frame next, with names
representing the cursor state to be selected. After all the components
have been added, the pack()
method is invoked so that the button placement can be optimized.
Since this optimized placement will result in a small frame (six
buttons sized tightly around the label text doesn't take much
space), the resize() method
is called to make the frame a larger size. Finally, the frame
is displayed with the show()
method.
Figure 4.1 : Frame applets for changing the state of the cursor.
When a button is selected, the custom method SetCursor()
is invoked. This method takes the button label and figures out
which cursor should be displayed. The Frame setCursor()
method is used to set the cursor state; its parameter is a static
integer defined as part of the Frame class.
| Note |
You might see the message Untrusted Java Applet Window in your Netscape browser when applet frames are displayed. However, this is not cause for alarm-it is just a security message.
|
Listing 4.1. Code for Frame applet that changes the cursor
state.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This applet simply starts up the frame used to
// show different frame cursors...
public class FrameCursorApplet extends Applet {
public void init() {
// Create the frame with a
title...
new FrameCursor("Frame
Cursors");
}
}
// The frame for letting the user pick different
// cursors to display...
class FrameCursor extends Frame {
// Create the frame with a title...
public FrameCursor(String title) {
// Call the superclass constructor...
super(title);
// Create a grid layout to
place the buttons...
setLayout(new GridLayout(3,2));
// Add the buttons for choosing
the cursor...
add(new Button("Default"));
add(new Button("Wait"));
add(new Button("Hand"));
add(new Button("Move"));
add(new Button("Text"));
add(new Button("SE Resize"));
// Pack and display...
pack();
resize(300,200); // Make it
a reasonable size...
show();
}
// Handle events...
public boolean handleEvent(Event e) {
switch(e.id) {
case e.WINDOW_DESTROY:
dispose(); //
Erase frame
return
true;
case
Event.ACTION_EVENT:
if
(e.target instanceof Button)
SetCursor((Button)e.target);
return
true;
default:
return
false;
}
}
// Set the cursor based on the button chosen...
void SetCursor(Button btn) {
// Get the label of the button...
String selection = btn.getLabel();
//Set the cursor based on
that label...
if (selection.equals("Wait"))
setCursor(Frame.WAIT_CURSOR);
else if (selection.equals("Hand"))
setCursor(Frame.HAND_CURSOR);
else if (selection.equals("Move"))
setCursor(Frame.MOVE_CURSOR);
else if (selection.equals("Text"))
setCursor(Frame.TEXT_CURSOR);
else if (selection.equals("SE
Resize"))
setCursor(Frame.SE_RESIZE_CURSOR);
else // Just use the default...
setCursor(Frame.DEFAULT_CURSOR);
}
}
The current state of the cursor can be retrieved with the getCursorType()
method, and the getTitle()
and setTitle() Frame methods
can be used for getting and setting the title bar caption, respectively.
Similarly, the getIconImage()
and setIconImage() methods
can be used to set the image display of an iconized frame.
Frames and menus are closely related since the Frame class is
the only AWT class with built-in support for menus. Frames implement
the MenuContainer interface, which is used to contain menu components.
These components are defined by the MenuComponent class, the superclass
of all menu components. It defines a set of methods pertinent
to individual menu items. For example, the getFont()
and setFont() MenuComponent
methods are used to control the font selection of the menu object.
Menus can be illustrated by modifying the previous example to
use menus, instead of buttons, to change the state of a cursor.
Listing 4.2 shows the code that performs this. The constructor
of the Frame class, called FrameMenuCursor, shows how to add menus
to a frame. The first step is creating a MenuBar object. The menu
bar is tied to the frame with the setMenuBar()
method of the Frame class. This makes the MenuBar object the default
menu for the frame.
The next step is to add the individual menus to the menubar. The
Menu class is used as a container of individual menu items or
other menus. In this applet, the two menus are File and Cursor.
The Menu constructor takes these strings as its sole parameter,
thus defining the text to be associated with the menu. The menus
are then added to the menu bar through the MenuBar add()
method. The remove() method
can be used to delete a Menu from a MenuBar.
The final step in menu creation is to add the individual menu
items, defined by the MenuItem class. This class is a subclass
of MenuComponent and provides additional operations to be performed
on a menu item. MenuItem methods can be used to set the menu label
and to disable or enable an item. The constructor for the MenuItem
has as its sole parameter the item label that will be initially
displayed. The add() method
is used to add an instance of MenuItem to a Menu.
Listing 4.2. Code for applet that uses menus to change cursor
state.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This applet simply starts up the frame
// which provides a menu for setting cursors...
public class FrameMenuCursorApplet extends Applet {
public void init() {
// Create the frame with a
title...
new FrameMenuCursor("Menu
Based Cursors");
}
}
// The frame for letting the user pick different
// cursors to display...
class FrameMenuCursor extends Frame {
// Create the frame with a title...
public FrameMenuCursor(String title) {
// Call the superclass constructor...
super(title);
// Add the menus...
// First create the menu bar
MenuBar mbar = new MenuBar();
setMenuBar(mbar); // Attach
to the frame...
// Add the File submenu...
Menu m = new Menu("File");
mbar.add(m); //
Add to menu bar
// Add Quit to the submenu...
m.add(new MenuItem("Quit"));
// Add the Cursor submenu...
m = new Menu("Cursor");
mbar.add(m); //
Add to menu bar
// Add the cursor selections
to the submenu...
m.add(new MenuItem("Default"));
m.add(new MenuItem("Wait"));
m.add(new MenuItem("Hand"));
m.add(new MenuItem("Move"));
m.add(new MenuItem("Text"));
m.add(new MenuItem("SE
Resize"));
// Pack and display...
pack();
resize(300,200); // Make it
a reasonable size...
show();
}
// Handle events...
public boolean handleEvent(Event e) {
switch(e.id) {
case e.WINDOW_DESTROY:
dispose(); //
Erase frame
return
true;
case e.ACTION_EVENT:
//
Process menu selection...
if
(e.target instanceof MenuItem) {
//
Get the name of the menu selection..
String
menuName = e.arg.toString();
//
Dispose of frame if quit is chosen...
if
(menuName.equals("Quit"))
dispose();
//
Otherwise, set the cursor...
if
(menuName.equals("Default"))
setCursor(Frame.DEFAULT_CURSOR);
if
(menuName.equals("Wait"))
setCursor(Frame.WAIT_CURSOR);
if
(menuName.equals("Hand"))
setCursor(Frame.HAND_CURSOR);
if
(menuName.equals("Move"))
setCursor(Frame.MOVE_CURSOR);
if
(menuName.equals("Text"))
setCursor(Frame.TEXT_CURSOR);
if
(menuName.equals("SE Resize"))
setCursor(Frame.SE_RESIZE_CURSOR);
return
true;
}
// end if
return
true;
default:
return
false;
}
}
}
A MenuItem can be removed from a Menu with the remove()
method, much as it is in the MenuBar class. A separator, dividing
menu items, can be added with the addSeparator()
method of the Menu class. This is useful for menus that have different
categories of options.
The current example also illustrates how to process menu selections.
When a menu item is selected, an ACTION_EVENT
is issued. The instanceof
operator can be used in the handler to verify that the target
of the action is a menu. By taking the Event argument (the arg
variable) and converting it to a string, the program can get the
name of the menu item. Through the name, the appropriate course
of action can then be taken. In this example, the Quit menu item
forces the frame to shut down by using the dispose()
method. The other menu choices result in a new state for the cursor.
It should be mentioned that the CheckboxMenuItem class can be
implemented for menus that need to use checkmarks to indicate
whether the item has been selected. This is handy for menu items
that toggle.
Like the Frame class, the Dialog class is a subclass of Window.
Dialogs differ from Frames in a couple of subtle ways, however.
The most important of these differences is that Dialogs can be
modal. When a modal Dialog is displayed, input to other
windows in the Applet is blocked until the Dialog is disposed.
This feature points to the general purpose of Dialogs, which is
to give the user a warning or a decision to be made before the
program can continue. Although non-modal, or modeless,
Dialogs are supported, most Dialogs are modal.
There are two constructors for the Dialog class. Both take a Frame
object as a parameter; they also take a boolean flag, which indicates
whether the dialog should be modal. If the flag is set to true,
the dialog is modal. The constructors differ only in a parameter
that specifies whether the dialog should have a title caption.
It is this constructor that is used in the example that follows.
Figure 4.2 shows a variation of the previous applet, except that
a Dialog is used to change the cursor state. Listing 4.3 shows
the code for the Dialog class, called ChangeCursorDialog. The
Frame class (still called FrameMenuCursor from the previous example)
declares the dialog and instantiates it as follows:
Figure 4.2 : Using Dialog to change the state of the cursor
ChangeCursorDialog dlg;
dlg = new ChangeCursorDialog(this,true,"Change the cursor");
The menu for this application is different from the previous example.
The frame constructs the menu as follows:
// First create the menu bar
MenuBar mbar = new MenuBar();
setMenuBar(mbar); // Attach to the frame...
// Add the File submenu...
Menu m = new Menu("File");
mbar.add(m); // Add to menu bar
// Add Dialog to the submenu...
m.add(new MenuItem("Cursor Dialog"));
// Add a separator
m.addSeparator();
// Add Quit to the submenu...
m.add(new MenuItem("Quit"));
Note that a separator is added to divide the two menu
items.
When the Cursor Dialog menu item is chosen, the dialog box is
presented with the following code:
if (menuName.equals("Cursor Dialog"))
dlg.show(); // Make the dialog visible...
Listing 4.3. Code for a Dialog to change the cursor
state.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// Dialog that presents a grid of buttons
// for choosing the Frame cursor. A Cancel
// button exits the dialog...
class ChangeCursorDialog extends Dialog {
FrameMenuCursor fr;
// Create the dialog and store the title string...
public ChangeCursorDialog(Frame parent,boolean modal,String title)
{
// Create dialog with title
super(parent,title,modal);
fr = (FrameMenuCursor)parent;
// The layout is Grid layout...
setLayout(new GridLayout(3,2));
// Add the button options
add(new Button("Default"));
add(new Button("Wait"));
add(new Button("Hand"));
add(new Button("Move"));
add(new Button("Text"));
add(new Button("Cancel"));
// Pack and size for display...
pack();
resize(300,200);
}
// Look for button selections to
// change the cursor...
public boolean action(Event e,Object arg) {
// If button was selected then exit dialog..
if (e.target instanceof Button) {
// And possibly
change the cursor...
if (arg.equals("Default"))
fr.setCursor(Frame.DEFAULT_CURSOR);
if (arg.equals("Wait"))
fr.setCursor(Frame.WAIT_CURSOR);
if (arg.equals("Hand"))
fr.setCursor(Frame.HAND_CURSOR);
if (arg.equals("Move"))
fr.setCursor(Frame.MOVE_CURSOR);
if (arg.equals("Text"))
fr.setCursor(Frame.TEXT_CURSOR);
dispose();
}
return false;
}
}
Another AWT class called FileDialog is a subclass of Dialog. It
is used for creating stock Load and Save dialog boxes. The upcoming
tutorial has examples of the FileDialog class.
The Color class is used to set an object's colors. This class
represents a color as a combination of RGB values. RGB is a 24-bit
representation of a color composed of red, green, and blue byte
values. This 24-bit representation is virtual since many platforms
do not support 24-bit color. In these situations, Java maps the
color to the appropriate value; this will usually be an index
into a palette.
The Color class defines a set of stock color values, implemented
as static variables. For example, Color.red is available with
an RGB value of 255,0,0. Other colors supported are white, black,
gray, green, blue, yellow, magenta, and cyan. Various Color constructors
can also be used to define other colors. For example, the statement
Color col = new Color(0,0,120);
produces a dark blue color. Other methods, such as darker()
and brighter(), can also
be used to create a new Color object from an existing one.
There are generally two approaches to tying a color to an object.
The first approach is to use methods provided by the Component
class. The setBackground()
and setForeground() methods
are used to set the colors of an object in the two respective
positions. For example, the following code creates a Panel object
and sets its background to white:
Panel p = new Panel();
p.setBackground(Color.white);
You can use the respective methods prefixed by "get"
to return the background and foreground colors of a component.
The other approach to setting a color occurs in the paint()
method. Recall that a Graphics object is passed to this method.
The setColor() method can
then be used to set the color of the next items to be painted.
For example, the following code paints a blue framed rectangle:
public synchronized void paint (Graphics
g)
g.setColor(Color.blue);
g.drawRect(0,0,100,40);
The tutorial in this section gives more examples of using color.
A dialog box presents a choice of colors through the use of colored
components. The spreadsheet is modified to use these chosen colors
when it is painted.
Fonts are a critical aspect of graphics-based programming. Not
only are they important for making an application attractive,
they are a seminal part of programming a visual interface. Often
how a component is sized or placed turns on what font is being
used. The spreadsheet tutorial in this chapter, for example, uses
the current dimensions of the font to determine how the spreadsheet
cell should be sized and located. Therefore, there are two aspects
of font programming: tying fonts to display components and getting
font information to program how graphical objects are positioned.
The former aspect generally falls in the domain of the Font class;
the latter is tied to the FontMetrics class, although Font is
involved here also.
To construct a Font object, you need a font name, style, and size.
The font name represents a family of fonts, such as Courier. The
font families available to an applet depend on where the program
is running, and so decisions about what font is to be used should
be made dynamically. The java.awt.Toolkit has a method called
getFontList() that returns
a string array of font names on the host system. An example of
how this method is used is found in the next section and in the
font dialog in the tutorial. The family of a Font can be retrieved
through the getFamily() method.
The size of a font is its point size, such as 8 for a small font
and 72 for a large one. There are three styles defined as constants
in the Font class: PLAIN,
BOLD, or ITALIC.
The latter two can be combined to produce a font with both features.
For example, you could create a Helvetica font that has a 36-point
size and is both bold and italicized as follows:
Font f = new Font("Helvetica",
Font.BOLD + Font.ITALIC,36);
Fonts can be tied to a component through the setFont()
method. To create the previous font and tie it to a label, the
following code could be invoked:
Label l = new Label("My Label");
l.setFont(new Font("Helvetica", Font.BOLD + Font.ITALIC,36));
A reference to a component's Font can be retrieved through getFont().
Another way to use fonts is in the paint()
method. The Graphics class used in paint()
can have the current font set through the setFont()
method. For example, Listing 4.4 provides the code for an applet
that paints a series of Strings containing a row number. Figure
4.3 shows the output. When the paint()
method begins, a new font is created and set to the graphics object;
this font is now the current font used for the following graphics
calls. The drawString() method
is then called repeatedly with the y coordinate being dynamically
calculated.
Figure 4.3 : A Font applet using hardcoded coordinates
Listing 4.4. Code for Font applet using hard-coded coordinates.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This aspect throws a string into the applet
// display separated by a height difference of the
// current font
public class FontTest extends Applet {
public void paint(Graphics g) {
int y;
Font f = new Font("Helvetica",Font.BOLD
+ Font.PLAIN,16);
g.setFont(f);
for (int i = 0; i < 5;
++i) {
y = 40 +
(i * 20);
g.drawString("Row
" + i + " of text output.",10,y);
} // end for
}
}
There are a couple of things about this applet that are not quite
optimal. First of all, it isn't good to create a new Font in the
paint routine. Painting occurs frequently and font creation is
an expensive operation. It is faster and more efficient to create
the font only once, as in the init()
method.
The other thing that might be avoided is using hard-coded values,
which is notorious for displays that do not appear uniformly across
different platforms. (After all, that is why AWT has layouts!)
Although this applet might look decent across platforms, it gives
an excuse for showing a better way of using coordinates. This
is where the FontMetrics class comes in.
FontMetrics is used to get a variety of information about a Font
object. Such information includes the font's dimensions, how big
a String or character using that font would be, and so forth.
Table 4.1 lists some of the methods of the FontMetrics class.
Table 4.1. FontMetrics methods.
| Method | Description
|
| charWidth
| Returns width of character using this font.
|
| GetAscent
| Returns the ascent of the font (distance between the baseline and top of the character).
|
| GetDescent
| Returns descent of font (distance from the baseline of the font and its bottom).
|
| getHeight
| Returns total height of font (ascent + descent + leading).
|
| getLeading
| Returns the line spacing of the font. |
| stringWidth
| Returns the number of pixels a String using this font will take.
|
You can retrieve a FontMetrics object for a specific font in a
couple of ways. First of all, the FontMetrics object can be created
from scratch by using its constructor; it takes the Font in question
as its only parameter. The reference to the FontMetrics can also
be taken directly from the Graphics class by using the getFontMetrics()
method. The first technique is used when the program is not painting,
but that is when the latter technique is applied. Finally, it
could retrieve from the default toolkit with a method also named
getFontMetrics().The next
section explains how the Toolkit class works.
Figure 4.4 shows an improved version of the Font applet through
the use of FontMetrics; Listing 4.5 gives the new code. In this
version of the applet, each of the String displays are equally
incremented by the height of the font. The code calculates the
height increment from the getHeight()
method of the FontMetrics reference of the Graphics object. The
new code is also more efficient than the previous example since
it creates the font only once, at initialization.
Figure 4.4 : A Font applet using FontMetrics.
Listing 4.5. Code for a Font applet that uses FontMetrics.
import java.awt.*;
import java.lang.*;
import java.applet.*;
// This aspect throws a string into the applet
// display separated by a height difference of the
// current font
public class FontTest extends Applet {
Font f;
// Create the font...
public void init() {
f = new Font("Helvetica",Font.BOLD
+ Font.PLAIN,16);
}
// Paint using TextMetrics to set the height
in even
// increments...
public void paint(Graphics g) {
// Set the font
g.setFont(f);
// Always increment by twice
the height of the font...
int heightIncrement = 2
* g.getFontMetrics().getHeight();
int y = 0;
for (int i = 0; i < 5;
++i) {
y += heightIncrement;
g.drawString("Row
" + i + " of text output.",10,y);
} // end for
}
}
The tutorial that follows will have even more examples of fonts,
including a dialog box that can be used to select a font family,
size, and style.
The AWT Toolkit class provides the link between the AWT system-independent
interface and the underlying platform-based toolkit (such as Windows).
The class is abstract, so the getDefaultToolkit()
method must be called to get a reference to the actual Toolkit
object. Once this is done, all kinds of information about the
underlying environment can be revealed. By working with the Toolkit,
this underlying information can be used in a way that does not
compromise portability.
For example, the following code gets a list of all the fonts available
on the native platform and prints them out:
String fontList[] = Toolkit.getDefaultToolkit().getFontList();
for (int i = 0; i < fontList.length;
++i)
System.out.println(fontList[i]);
If the fonts used in an applet are derived from this list, versus
using hard-coded font names, the portability of an applet will
be improved. The getFontMetrics()
method returns the screen metrics of a font.
Another useful Toolkit method is getScreenSize().
This returns the full-screen dimensions of the native platform.
If you want to create a window that fills up the screen, these
dimensions will be useful. The getScreenResolution()
method returns the dots-per-inch resolution of the screen.
The Toolkit class also provides a variety of methods for using
images, including a method for synchronizing the screen's graphics
state. This sync() method
can be used in animation to avoid flicker.
Streams are a traditional way of representing a connection
between two objects. The types of streams that programmers are
most familiar with are ones between I/O devices (such as files)
and processes or streams between two memory structures. Each endpoint
of a stream connection can have one or two channels: an input
channel and an output channel. When a process is reading from
a file, it reads from a stream that receives data from the file
device's output channel. From the point of view of the process,
it is managing an input stream. From the viewpoint of the
file device, it is sending an output stream. Figure 4.5
illustrates the relationship.
Figure 4.5 : Stream relationships when a process reads from a file.
The JDK package for managing input and output interactions, called
java.io, has the stream concept at its foundation. It is structured
on the notion of input and output streams just discussed. These
stream classes permeate the Java programming environment. Their
use ranges from file I/O to network programming to the widely
used System.out.println()
method. There are few things that will increase the capabilities
of a Java programmer more than a full understanding of Java streams.
As just stated, the java.io package is based heavily on the notion
of input and output streams. This is made clear by the diagram
of significant java.io classes, shown in Figure 4.6. The most
important of these are, not surprisingly, the InputStream and
OutputStream classes, which are generally used to read and write
from a stream, respectively. They form the basis of an elaborate
network of stream classes that make it easy to build high-level
streams, such as those that work on newline delimited ASCII data,
to low-level streams, such as one that manages data on a byte-by-byte
level. How this works will be explored in the upcoming discussion
of the two foundation classes and their subclasses.
Figure 4.6 : Significant classes of the java to hierarchy.
Before engaging in a discussion of the input and output stream
classes, it's useful to look at some of the other aspects of Java
I/O programming. The File class can be used to get information
about a file or directory on the host file system. For example,
the following code checks to see whether a file exists in the
current directory and lists what is in the grandparent directory.
myFile = new File("File.txt");
System.out.println("The
file " + myFile.getName()
+
" existence is " + myFile.exists());
File myDirectory = new File("..\\..");
String s[] = myDirectory.list();
System.out.println("Directory
contents: ");
for (int i = 0; i < s.length;
++i)
System.out.println(s[i]);
As you might have observed in the constructor for the directory
object, the naming conventions are based on the host file system.
Consequently, hard coding such conventions can result in nonportable
code. Fortunately, the class provides some constants that can
hide the platform-specific conventions. For example, the following
code makes the directory constructor portable:
File myDirectory = new File(".."
+ File.separator + "..");
The File class also offers a variety of other file operations.
This includes the ability to delete or rename a file, create a
new directory, and inspect a file's attributes. Note, however,
that these operations are subject to the native environment's
security constraints, discussed in the following section.
The Java language and its native environment (such as a browser)
give you a sophisticated system of security layers. At the top
of this security hierarchy is protection for the local file system.
After all, the most prevalent methods for a virus to inflict pain
on its victim is to destroy his or her file system. Consequently,
protection of the file system where the applet is running is paramount.
A Java environment enforces a security policy through its security
manager. This is implemented as an object through the SecurityManager
class and is created once and only once by a browser at startup.
Once established, a SecurityManager cannot be changed or replaced.
The reasons for this are obvious. If a rogue applet can modify
the SecurityManager object, then it can simply remove all access
restrictions and wield unlimited power over your hard disk. However,
it is possible to get a reference to the SecurityManager through
the System object's getSecurityManager()
method. Once obtained, methods can be applied on the object to
see what security constraints are currently being applied.
The strictness of a security policy depends on the runtime Java
environment. Netscape Navigator, for example, cannot perform any
file operations. The simple File class example in the previous
section causes a security violation. This occurs when the File
exists() method is invoked,
which is the first file operation in the code (the File constructor
simply associates the String name with the object).
Sun's HotJava browser allows some file operations on the client
running an applet. Note that where the applet is loaded from is
important. The client is the site running the applet; the server
is where the applet was loaded from. The security that concerns
Java the most is that of the client site. In Hot Java, the operations
allowed on the client are based on the concept of access control
lists. By default, any file not covered in the list cannot
be accessed in any fashion, including the simple File exists()
operation. The access control list can be found in the properties
file of the .hotjava directory located under the parent.
File security is even weaker for the appletviewer program that
programmers can use to test an applet. In this environment, most
file operations are allowed for applets loaded from the client.
Some file operations performed by applets loaded from a server
over the network are also permitted. Standalone Java applications
have no file security. If a sophisticated Java application is
being developed, such as over an internal corporate network, it
probably would be best to create a subclass of SecurityManager
to set up a security policy appropriate for that environment.
When a file operation violates a security policy, a SecurityException
object is thrown. This can be caught by the program to prevent
abnormal termination.
Besides thrown SecurityException objects, Java I/O programs need
to be concerned with handling other exceptions. In particular,
they need to catch IOException objects that are thrown. The IOException
class embraces the category of errors related to input/output
operations. Many of the methods in the java.io package throw IOException
objects that must be caught. The typical structure of code that
performs I/O operations is, therefore, as follows:
try {
// IO operations
}
catch (IOException e) {
// Handle the error
}
Two subclasses of IOException are noteworthy. A FileNotFoundException
object is thrown when there is an attempt to open a file that
doesn't exist. For example, the constructor for the FileInputStream
class tries to open the file specified in its parameter. If it
cannot do so, an object of class FileNotFoundException is thrown.
Another notable subclass of IOException is EOFException, which
typically occurs in read operations when an end of file has been
reached. This indicates that there are no more bytes to be read.
Figure 4.7 illustrates the class hierarchy that descends from
the InputStream class. InputStream is an abstract class that defines
the basic operations that must be implemented by its subclasses.
The most noteworthy of these is the read()
method. This is used to read in bytes one at a time or into a
byte array. The System.in variable, which represents that standard
input stream, is based on the InputStream class and is now used
to illustrate the read()
method:
Figure 4.7 : Input stream classes
try {
int b;
b = System.in.read();
}
catch (IOException e) {
// Handle any exception
}
This example reads in a byte that is, confusingly, converted to
an integer by the read()
method. The method returns -1
if there is no input to be read. For many subclasses of InputStream,
an EOFException object is thrown in these circumstances.
Table 4.2 lists other notable methods of the abstract InputStream
class.
Table 4.2. Notable InputStream methods.
| Method | Description
|
| available
| A non-blocking way to find out number of bytes available to read.
|
| Close |
Closes the input stream. |
| mark |
Marks the current stream position. |
| read |
Reads a byte or array of bytes. |
| reset |
Resets stream to last marked position. |
| skip |
Skips over specified number of input bytes. |
FilterInputStream classes
The FilterInputStream class is at the top of a branch of the InputStream
hierarchy that can be used to implement some very powerful operations.
In particular, this class is used to chain a series of FilterInputStream
classes so that one stream can process data before passing it
"up" to the next stream. This chaining technique is
excellent to use when encapsulating classes that interact with
streams at a lower level (such as bytes) into streams that read
in data at a higher level, such as Strings. The following example
illustrates how this works.
As seen in the previous overview of the InputStream class, the
System.in object reads data in a byte at a time. This will prove
to be cumbersome for many standard input operations that typically
want to work on String objects. Fortunately, FilterInputStream
classes make it easy to abstract this byte-level functionality
into a higher and more usable interface. The code in Listing 4.6
takes the System.in object and creates a DataInputStream object
that can be used to read in a String of text delimited by a newline
or EOF. This is then sent to the System.out object, which prints
the string to the standard output stream.
Listing 4.6. Chaining Input Stream classes.
try {
// Create
the data input stream...
DataInputStream
dis =
new
DataInputStream(
new
BufferedInputStream(System.in));
// Read
in a line of text...
String s
= dis.readLine();
// Send
it to standard output...
System.out.println(s);
}
// Handle any exceptions caused
by the process...
catch (IOException e) {
// Just
print out the error...
System.out.println(e.getMessage());
}
The first line inside the try
clause is full of material; the innermost part of the clause introduces
the BufferedInputStream class:
new BufferedInputStream(System.in)
This subclass of FilterInputStream simply allows the reading in
of data more efficiently. Reading a file so that every byte request
requires a new input read is very slow. The BufferedInputStream
class reads data into a buffer in a series of large chunks. This
prevents each request for a byte of data resulting in a read from
the underlying input stream, such as a file or network read. If
the BufferedInputStream already has the data in the buffer, it
can return the data directly from its memory cache. This is also
useful for operations that require movement back and forth in
a stream, such as a "push back" of an unneeded byte.
Since this class will improve the efficiency of input stream operations,
it should be used liberally.
Once the BufferedInputStream object is created, it is used as
the constructor for a DataInputStream object:
DataInputStream dis =
new DataInputStream(
new
BufferedInputStream(System.in));
This code means that the DataInputStream object's request for
input is actually made to a BufferedInputStream object. This object
in turn makes its requests for input to the System.in object (which
is based on InputStream) when it needs more data.
The code can now use DataInputStream methods, such as readLine(),
to hide the underlying request of data at a byte-by-byte level.
The DataInputStream methods return the data formatted in a data
type that the user needs. In this case, readLine()
reads in a String of text delimited by a new Line (a carriage
return/line feed, or EOF). The delimiter is not included in the
stream.
The DataInputStream class supports over a dozen data type operations,
as listed in Table 4.3. These include most of the basic data types
supported from Java, ranging from a single byte to an integer
to a floating point number to a full-text String in Unicode format.
The DataInputStream class is an implementation of the DataInput
stream interface, which defines the data type methods that need
to be supported.
Table 4.3. Data-type reads supported by DataInputStream.
| Method | Description
|
| read |
Reads data into byte array. |
| readBoolean
| Reads a boolean. |
| readByte
| Reads an 8-bit byte. |
| readChar
| Reads a 16-bit char. |
| readDouble
| Reads a 64-bit double number. |
| readFloat
| Reads a 32-bit floating point number. |
| readInt
| Reads a 32-bit integer. |
| readLine
| Reads String terminated by newline or EOF.
|
| readLong
| Reads a 64-bit long. |
| readShort
| Reads a 16-bit short. |
| readUTF
| Reads a Unicode formatted String. |
| readUnsignedByte
| Reads an unsigned byte. |
| readUnsignedShort
| Reads an unsigned 16-bit short. |
From the powerful chaining operation supported by FilterInputStream,
it's easy to imagine interesting new possible classes. For example,
a WordProcessStream could be written to read paragraphs and images
into a word processing application.
Two other FilterInputStream classes are worth mentioning. The
LineNumberInputStream class associates the data streams with line
numbers. This means that the class supports such operations as
setLineNumber() and getLineNumber().
The PushBackInputStream class is used for the kind of operations
that parsers typically perform. Parsers need methods such as unread(),
which pushes a character back into a stream. This kind of operation
is needed when a token has been identified by the parser.
Other InputStream classes
Although the FilterInputStream classes are the most powerful part
of the InputStream hierarchy, other classes are also useful. If
applets read from a file, an instance of the FileInputStream class
will probably be used. It includes the same operations as its
superclass, InputStream. Therefore, it can be used to pass data
to FilterInputStream classes, as in the previous section's example.
Listing 4.7 shows how to use chaining to read an ASCII file and
send its contents to standard output. Instead of creating the
FilterInputStream classes from the System.in InputStream object,
it is built from a FileInputStream object tied to the filename
provided in the method parameter; if the file cannot be opened
because it does not, an IOException object will be thrown.
Listing 4.7. Reading a file to standard output.
void readFileToStandardOut(String filename)
{
System.out.println("*****
BEGIN " + filename + " *****");
try {
// Open
the file...
DataInputStream
dis = new DataInputStream(
new
BufferedInputStream(
new
FileInputStream(filename)) );
// Read
lines until EOF is reached...
String s;
while((s
= dis.readLine()) != null)
System.out.println(s);
// Close
the file when done...
dis.close();
}
// Handle any exceptions caused
by the process...
catch (IOException e) {
// Just
print out the error...
System.out.println(e.getMessage());
}
System.out.println("*****
END " + filename + " *****");
}
As this example further illustrates, the powerful technique of
stream chaining makes it easy to abstract away from the low-level
implementation of reading from a file. In fact, except for the
innermost part of the constructor, with this code it really doesn't
matter if you're working on a file, the standard input stream,
or even from a network!
However, if it's necessary to work with streams at the level of
bytes, the ByteArrayInputStream class could be used. This takes
a large array of bytes and uses it as the basis for input stream
requests. Therefore, this is the constructor for the class:
ByteArrayInputStream bis = new ByteArrayInputStream(byteArray)
The reset() method sets the
current position back to the beginning of the stream buffer. The
StringBufferInputStream class functions in a manner similar to
ByteArrayInputStream, except it works on a StringBuffer. Like
ByteArrayInputStream, however, the read()
methods return a single byte or an array of bytes.
The PipedInputStream class works with the PipedOutputStream class
to send a stream of data from one channel to another. This is
well suited for thread processing, in which one thread is producing
data (the output) to another thread that's consuming it (the input).
Piped streams is a classic technique of interprocess communication.
Chapter 8, "Adding Threads to Applets,"
will provide a concrete example of piped streams.
Finally, the SequenceInputStream class can be used to take a series
of InputStreams and treat them as if they were one stream. This
could be useful for operations such as concatenating files.
The OutputStream class is an abstract class that marks the top
of the hierarchy of classes used for streamed output. Its major
operations are represented by write()
methods, which can be used to write an integer, a byte, or an
array of bytes to a stream. The other methods of OutputStream
are the close() method, which
is used to close a stream, and the flush()
method, which writes the contents of a buffered cache to the stream-this
applies only to derivatives of OutputStream that implement buffering.
Figure 4.8 lists the output classes that descend from OutputStream.
In many cases, the classes mirror the input classes shown in Figure
4.7. The most noteworthy of these, not surprisingly, is the FilterOutputStream
class, which, like its FilterInputStream counterpart, is used
to chain streams together. This class will be used as the starting
point for a tour through the OutputStream classes.
Figure 4.8 : Output Stream classes
The FilterOutputStream class is used to chain streams together
so that one stream can process data before passing it to the next
stream in the chain. This is useful for cases in which the lowest
level stream reads in data at a primitive level (such as bytes)
and passes up to the interface layer, which manages streams in
terms of Strings and other data types. This is well suited for
operations such as writing to a file, as the following example
illustrates.
Listing 4.8 shows a method that takes a string array and writes
it to a file, which is formatted for ASCII text. As in the InputStream
example of Listing 4.7, the key part of this method is the constructor.
Its innermost statement consists of a constructor for a stream
that writes to a file:
new FileOutputStream(filename)
Listing 4.8. Writing a string array to an ASCII file.
void writeStringsToFile(String s[],String
filename) {
try {
// Create
the output stream to a file...
DataOutputStream
out = new DataOutputStream(
new
BufferedOutputStream(
new
FileOutputStream(filename)) );
// Write
the strings out...
for (int
i = 0; i < s.length; ++i)
out.writeBytes(s[i]);
// Flush
and close...
out.flush();
out.close();
}
// Handle any exceptions caused
by the process...
catch (IOException e) {
// Just
print out the error...
System.out.println(e.getMessage());
}
}
The FileOutputStream opens a file with the designated filename.
If the file previously exists, its contents will be lost. The
FileOutputStream class has an alternate constructor that takes
a File object. The File class was described in the section "Structure
of the java.io Package." The created file can then be written
to with the write() methods,
which can take a single integer or a byte array.
In the example, the FileOutputStream is fed as input into the
BufferedOutputStream. The use of this class is very useful for
file write operations since it prevents disk writes from occurring
every time the buffer is given some data. It only writes when
the buffer's cache is full or when the flush()
method is called.
Finally, the newly created BufferedOutputStream object becomes
the source for the DataOutputStream object. Like the DataInputStream
counterpart, the DataOutputStream works with general Java data
types, such as bytes, integers, doubles, and Strings. Table 4.4
summarizes the write operations supported by DataOutputStream.
These are fairly straightforward, except for the three methods
used for writing out Strings. Recall that Java treats characters
not as 8-bit ASCII bytes, but as 16-bit Unicode characters. This
is important for creating international software. However, programmers
often need to read and write data in its native form. The writeBytes()
method takes a String and writes out each character in the 8-bit
ASCII format. This is the method used in the example. The writeChars()
method, on the other hand, writes data out in 16-bit Unicode characters.
A file created this way will not look or behave like an ASCII
file, so this method wasn't used in the example. The writeUTF()
method writes a String out in a special Unicode format.
Table 4.4. Data-type writes supported by DataOutputStream.
| Method | Description
|
| write(byte)
| Writes a byte. |
| Write(byte [],int, int)
| Writes a subarray of bytes. |
| WriteBoolean(boolean)
| Writes a boolean. |
| WriteByte(int)
| Writes an 8-bit byte. |
| WriteBytes(String)
| Writes a String out as 8-bit bytes. Use for writing out ASCII text.
|
| WriteChar(int)
| Writes a 16-bit char. |
| WriteChars(String)
| Writes a String out as 16-bit Unicode characters.
|
| WriteDouble(double)
| Writes a 64-bit double number. |
| WriteFloat(float)
| Writes a 32-bit floating point number. |
| WriteInt(int)
| Writes a 32-bit integer. |
| WriteLong(long)
| Writes a 64-bit long. |
| WriteShort(int)
| Writes a 16-bit short. |
| WriteUTF(String)
| Reads a Unicode formatted String. |
The DataOutputStream class implements the DataOutput class. This
defines the data-type write methods that need to be supported
by the classes that implement data output streams.
At the end of the example, there are calls to flush()
and close(). The flush()
method forces the BufferedOutputStream object to send its cached
contents to the FileOutputStream. This could also be called inside
the write loop at appropriate
intervals (such as when a prespecified number of bytes or Strings
have been written). The close()
method call closes in the FileOutputStream stream, and the file
is now complete.
The other FilterOutputStream class is PrintStream. This is the
class used to implement the standard output and error objects,
System.out and System.err. Of course, its most noteworthy method
is println(). This is actually
an overloaded method that takes any of a variety of data types
and writes it to the PrintStream (usually standard output) followed
by a new line. The println()
method is most widely used with String objects. This method is
also noteworthy in that the stream is flushed every time it is
called. The print() method
functions similarly but with no newline appended. A println()
or flush() method will cause
output from print() to be
written to the stream. Finally, a byte-based write()
is also supported by PrintStream.
The PipedOutputStream class works with the PipedInputStream class
to send a stream of data from one channel to another. Pipes are
a classic technique of interprocess communication. You will find
a concrete example of piped streams in Chapter 8.
The ByteArrayOutputStream is used for working with arrays of bytes.
Its most interesting feature is that its internal array will grow
as it receives data.
Two other I/O classes need to be mentioned. The RandomAccessFile
class implements both the DataInput and DataOutput interfaces.
Consequently, it is the only stream class that supports both the
read and write operations. This is good for more traditional programming
in which files are read and updated interactively. The RandomAccessFile
also supports file-positioning operations, such as seek().
The StreamTokenizer stream can be used for tokenizing a stream.
This is useful for the lexical analysis operations of a parser.
The spreadsheet applet from the previous chapter is enhanced in
this tutorial to support dialog boxes and to introduce file storage
to the spreadsheet data. Some new classes were written to create
the dialog boxes, and classes from the previous section were modified
to support new functioning.
For ease of reading, Figure 4.9 shows the spreadsheet applet again
as it appears in a browser.
Figure 4.9 : The spreadsheet applet.
Table 4.5 enumerates the classes used in this chapter's version
of the spreadsheet applet. Since many of these classes were created
in the previous section, the new classes are delimited by having
their name in bold. The classes that were modified have their
names italicized.
Table 4.5. 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. Can write or read its contents from a file.
|
| 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 poorly constructed.
|
| ArgValue | A FormulaParser helper class used to store information about an argument in a formula.
|
| ChooseColorDialog | Dialog for choosing the color of a spreadsheet element.
|
| ColorDisplay | A canvas class that ChooseColorDialog uses to display how a spreadsheet cell would appear if certain colors were picked.
|
| ColoredCheckbox | Draws a checkbox with its color set to a different color.
|
| ColoredCheckboxGroup | Creates a group of colored checkboxes.
|
| ChooseFontDialog | Dialog for choosing a font for the spreadsheet.
|
| FontDisplay | A canvas class that ChooseFontDialog uses to display how a font looks.
|
| 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. Sets fonts and colors of SpreadsheetCells.
|
| 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 menu for actions such as invoking dialog boxes.
|
| SpreadsheetApplet | Responsible for creating, showing and hiding the SpreadsheetFrame that provides the visual display of this applet.
|
The color dialog box lets you associate a color with various elements
of a spreadsheet cell. You can set the foreground and background
color of a cell in its normal state and also change the colors
of the cell when it is highlighted. Figure 4.10 shows how
the dialog appears in a browser.
Figure 4.10 : The color dialog box.
How the Dialog Box Is Used
When the dialog box appears, the current settings of the spreadsheet
cell colors are displayed. A list control in the top left shows
which elements of the spreadsheet cell can be modified. These
colors are normal foreground, normal background, highlighted foreground,
and highlighted background. When you select a list item, the radio
button for the corresponding color is highlighted. A text display
underneath the list shows what the spreadsheet cell would look
like with the given colors. The text display shows either the
normal state (with both foreground and background colors) or the
background state. If you select a new color with the radio button,
the canvas object is updated to show what the new foreground and
background combination would look like. The spreadsheet is updated
with the color settings for the current list item when you select
the Update button.
The Construction of the Color Dialog Box
Four classes are used to construct the color dialog box. The ChooseColorDialog
class is a subclass of Dialog and controls the main display and
control of the dialog box. The colorDisplay class is a Canvas
class derivative that draws text with colors corresponding to
the selected foreground and background display. The ColoredCheckbox
class draws a checkbox associated with a certain Color object;
the background of the box is drawn according to that color. The
ColoredCheckboxGroup class groups ColoredCheckbox items together
so that they can function as part of a radio button group.
The discussion of the color dialog box begins with its underlying
components. Listing 4.9 shows the code for the ColoredCheckbox
class. Its most interesting feature is that it associates itself
with a given Color object. It paints its background according
to the Color and, through the setIfColorMatches()
method, turns its checkbox on if the Color sent to it matches
its internal color. The checkbox in this case is a radio button
because the class is associated with a CheckboxGroup. Checkbox
objects have radio buttons only if they are associated with a
CheckboxGroup object; only one radio button in a checkbox group
can be on at one time. If no checkbox group is specified for a
checkbox object, then there are no restrictions on which boxes
can be selected.
Listing 4.9. The ColoredCheckbox class.
// Class for creating a checkbox associated
// with a given color...
class ColoredCheckbox extends Checkbox {
Color color; // The color of this
checkbox...
// Constructor creates checkbox with specified
color...
public ColoredCheckbox(Color color, String label,
CheckboxGroup
grp, boolean set) {
// Call
the default constructor...
super(label,grp,set);
this.color
= color;
setBackground(color);
}
// Sets itself to true if it matches the color
public void setIfColorMatches(Color match) {
if (color == match)
setState(true);
else
setState(false);
}
// Return the color matching this box...
public Color getColor() {
return color;
}
}
The ColoredCheckboxGroup is used to contain ColoredCheckbox objects.
Its constructor creates a preselected number of colored checkboxes
to be associated with a Panel object. Here are the first few lines
of the ColoredCheckboxGroup class declaration:
class ColoredCheckboxGroup extends CheckboxGroup
{
// Array to hold checkboxes...
ColoredCheckbox c[] = new ColoredCheckbox[12];
// Constructor. Create the checkboxes with
// no default color chosen...
public ColoredCheckboxGroup(Panel p) {
// Call the default constructor...
super();
// Create the checkboxes and
store in panel and reference array...
c[0] = new ColoredCheckbox(Color.black,"Black",this,false);
p.add(c[0]);
c[1] = new ColoredCheckbox(Color.cyan,"Cyan",this,false);
p.add(c[1]);
Strangely enough, ColoredCheckboxGroup is not a Container object.
Consequently, the checkboxes need to be associated with a Panel
to meet the needs at hand. Note the use of the Color constants
in constructing the ColoredCheckbox objects. The reference array
(variable c) is used in the
other method of the class, setMatchingColor(),
which is used to set the radio button of the ColoredCheckbox that
matches a certain color:
public void setMatchingColor(Color match)
{
for (int i = 0; i < c.length;
++i)
c[i].setIfColorMatches(match);
}
Since ColoredCheckbox objects are self-identifying by color, this
technique prevents a long and cumbersome walk through hard-coded
color names to see which radio button should be turned on.
The colorDisplay class is a Canvas derivative that draws text
(specified in the displayText
String variable) with a specified foreground and background color.
Listing 4.10 highlights some of the more interesting features
of the class. The paint()
method draws the canvas if a background and foreground color have
been selected. It first starts getting the size of its drawing
area through the size() method;
this method is a standard part of subclasses of Component. It
fills in the background color through the setColor()
and fillRect() methods of
the Graphics class. It then sets the color of the text to be displayed
(the foreground). By getting the current FontMetrics, the canvas
can figure out a good location for the text string; the getHeight()
method returns the total height of the font. The drawString()
method then draws the text at the specified location.
Listing 4.10. Portions of the colorDisplay class.
// The layout will call this to get the
minimum size
// of the object. In this case, you
want it to be at
// least big enough to fit the display test...
public Dimension minimumSize() {
// Get the metrics of the
current font...
FontMetrics fm = getFontMetrics(getFont());
return new Dimension(fm.stringWidth(displayText),
2 * fm.getHeight());
}
// Paint the colors and text...
public synchronized void paint(Graphics g) {
if ((foreground == null) ||
(background == null))
return;
// Set background...
Dimension dm = size();
g.setColor(background);
g.fillRect(0,0,dm.width,dm.height);
// Draw the string
g.setColor(foreground);
// Set dimensions. Move just
from left...
FontMetrics fm = getFontMetrics(getFont());
int x = fm.charWidth('W');
// And center in height...
int y = fm.getHeight();
g.drawString(displayText,x,y);
}
The minimumSize() method
is used with layouts, which were discussed in the last chapter.
When AWT is constructing the display of a group of components,
it works with the layouts to decide what position and size a component
should have. Sometimes, you might want a component to exercise
some input into what its size will be. Two methods of the Component
class can be invoked to do this. The preferredSize()
method returns the Dimensions of the preferred size of the component.
The minimumSize() method
returns the smallest size in which the component should be made.
In the case of the colorDisplay()
class, it returns that the component should be wide enough to
display the text String and twice as high as its current font.
It does this by getting the FontMetrics of the current font and
calling the stringWidth()
and getHeight() methods respectively.
Finally, the dialog box is ready to be constructed. Figure 4.11
highlights the declarations and methods used to construct the
color dialog box. The createComponents()
method adds the components to the dialog box by using the complex
GridBagLayout() class. The
main thing that needs to be done here is to have most of the space
taken up by the list control set to half the dialog box and the
color checkboxes on the other half, as shown in Figure 4.10. The
key reason for doing this is to set the GridBagConstraints weighty
and gridheight variables
to the appropriate values. By setting the former to 1.0,
this tells the layout that the associated components should be
given preeminence in terms of the layout's height. When weighty
is set to 0.0, then the height
of the corresponding components is given lower priority.
Figure 4.11 : The font dialog box.
The preferredSize() method
in Listing 4.11 returns the desired dimensions of the dialog box.
It needs to be three times as wide as the longest string in the
list component, and 24 times as high as the current font. This
way everything should fit comfortably in the dialog box.
Listing 4.11. The construction of ChooseColorDialog.
// Dialog box for choosing display colors...
public class ChooseColorDialog extends Dialog {
SpreadsheetFrame fr; // What
to update...
ColoredCheckboxGroup colorGrp; //
To hold radio buttons of colors...
SpreadsheetContainer s;
List choiceList; // List of color
choices...
colorDisplay d; // This is the text display...
// Defines for listbox values...
static int NORMAL_FORE = 0;
static int NORMAL_BACK = 1;
static int HILITE_FORE = 2;
static int HILITE_BACK = 3;
// Construct dialog to allow color to be chosen...
public ChooseColorDialog(Frame parent,boolean
modal) {
// Create dialog with title
super(parent,"Color Dialog",modal);
fr = (SpreadsheetFrame)parent;
// Create the dialog components...
createComponents();
pack(); // Compact...
// Resize to fit everything...
resize(preferredSize());
}
// The layout will call this to get the preferred size
// of the dialog. Make it big enough
for the listbox text
// the checkboxes, canvas, and buttons...
public Dimension preferredSize() {
// Get the metrics of the
current font...
FontMetrics fm = getFontMetrics(getFont());
int width = 3 * fm.stringWidth("Highlighted
foreground");
int height = 24 * fm.getHeight();
return new Dimension(width,height);
}
// Create the main display panel...
void createComponents() {
// Use gridbag constraints...
GridBagLayout g = new GridBagLayout();
setLayout(g);
GridBagConstraints gbc = new GridBagConstraints();
// Set the constraints for the top objects...
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1.0;
gbc.weighty = 1.0;
gbc.gridheight = 10;
// Add the listbox of choices...
choiceList = new List();
choiceList.addItem("Normal foreground");
choiceList.addItem("Normal background");
choiceList.addItem("Highlighted foreground");
choiceList.addItem("Highlighted background");
g.setConstraints(choiceList,gbc);
add(choiceList);
// Create the checkbox panel
Panel checkboxPanel = new Panel();
checkboxPanel.setLayout(new GridLayout(12,1));
// Create the checkbox group and add radio
buttons...
colorGrp = new ColoredCheckboxGroup(checkboxPanel);
colorGrp.setMatchingColor(Color.magenta);
// Create checkbox panel to right...
gbc.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(checkboxPanel,gbc);
add(checkboxPanel);
// Display the color chosen...
d = new colorDisplay("This is how
the text looks.");
// Add to grid bag...
gbc.weighty = 0.0;
gbc.weightx = 1.0;
gbc.gridwidth = GridBagConstraints.REMAINDER;
gbc.gridheight = 1;
g.setConstraints(d,gbc);
add(d);
// Two buttons: "Update" and
"Cancel"
Panel p = new Panel();
p.add(new Button("Update"));
p.add(new Button("Cancel"));
// Add to grid bag...
gbc.gridwidth = GridBagConstraints.REMAINDER;
g.setConstraints(p,gbc);
add(p);
}
Using the Dialog Box
Once the dialog box is displayed, its event loop is entered. Listing
4.12 details the handleEvent()
method of the dialog box. This code has several subtleties worth
noting. Most of the work is performed when an action occurs, as
indicated by the ACTION_EVENT
method. When a button is selected, the argument of the Event object
is set to the name of the button. The handleEvent()
method looks at this name to decide what to do. If the name is
"Cancel," then the dialog box is removed from the screen
with the dispose() method,
and control returns to the calling frame. The Update button sets
the spreadsheet colors according to what is currently highlighted.
How this works will be discussed shortly.
Listing 4.12. The handleEvent()
method of ChooseColorDialog.
// Wait for Cancel or OK buttons to be
chosen...
public boolean handleEvent(Event e) {
switch(e.id) {
case Event.ACTION_EVENT:
// Kill
the dialog...
if (e.arg.equals("Cancel"))
{
dispose(); //
Remove Dialog...
return
true;
} //
end if
// Update
colors on the spreadsheet...
if (e.arg.equals("Update"))
{
setSpreadsheetColors();
return
true;
} //
end if
if (e.target
instanceof Checkbox) {
selectedRadioItem();
return
false;
}
return false;
// User selected a listbox
item...
case Event.LIST_SELECT:
// Set up
caption colors and color choice highlight...
if (e.target
instanceof List) {
selectedChoiceListItem();
return
false;
} //
end list if
return false;
default:
return false;
} // end switch
}
If the target of the ACTION_EVENT
is a Checkbox, then it means a radio button is selected, which
calls the selectedRadioItems()
method. This method sets the colorDisplay object's foreground
or background color according to the radio button chosen and the
current selection in the list.
If you click on a list item, a LIST_SELECT
event is issued. In this case, this invokes the selectedChoiceListItems()
method. It sets the colorDisplay object's settings according to
the current list selection; this process needs to take into account
both the foreground and background colors. The ColoredCheckboxGroup's
setMatchingColor() method
is called to set the radio button of the ColoredCheckbox object
corresponding to the current list color.
See this book's accompanying CD-ROM for the full source code of
the selectedRadioItems()
and selectedChoiceListItems()
methods.
Calling the Dialog Box
The SpreadsheetFrame object is responsible for bringing up the
color dialog box. It declares a variable of the color dialog class
as follows:
ChooseColorDialog colorDialog; //
Color Dialog...
In its constructor, the frame instantiates the color dialog box
with the following:
colorDialog = new ChooseColorDialog(this,
true);
This states that the frame is the parent of the dialog box and
its appearance is modal. Recall that a modal dialog box does not
allow input to other windows while it is being displayed.
A dialog box does not automatically appear when it is constructed;
you do this with the show()
method. The SpreadsheetFrame object is modified in this chapter's
tutorial to add menu options for the new dialog boxes. Here is
some of the code from the frame's handleEvent()
method that results in the ChooseColorDialog object being displayed:
public boolean handleEvent(Event evt)
{
switch(evt.id) {
case Event.ACTION_EVENT: {
String menu_name = evt.arg.toString();
if (evt.target instanceof MenuItem) {
// Set colors...
if(menu_name.equals("Colors..."))
colorDialog.show();
//
other menu choices
The color dialog box overrides the show()
method so it can do some setup before the dialog box appears:
public synchronized void show() {
super.show(); // Call the
default show method...
// Get the spreadsheet container...
s = fr.getSpreadsheetContainer();
// Set the listbox default...
choiceList.select(0);
// Set up caption colors and
color choice
highlight...
selectedChoiceListItem();
}
After calling the superclass show method, the dialog box gets
a reference to the frame's SpreadsheetContainer object. It will
need this to get and set the spreadsheet's colors. It then sets
the list box selection to the first item. The last method called,
selectedChoiceListItem(),
sets the radio buttons and display canvas to values corresponding
to the current list selection.
Setting the Spreadsheet Colors
The SpreadsheetContainer class needs to be modified slightly to
support dynamically selecting colors. Fortunately, these changes
are small since the SpreadsheetCell class, which represents the
individual cells in the spreadsheet, was written to support colors
in its first version presented in Chapter 3,
"Building a Spreadsheet Applet."
Four Color variables are
introduced to the SpreadsheetContainer class so that colors can
be set dynamically:
Color normalForeColor = Color.black;
Color normalBackColor = Color.white;
Color highlightedForeColor = Color.white;
Color highlightedBackColor = Color.red;
Associated with the variable declarations are the default colors.
These SpreadsheetCell colors have the background and foreground
colors set, respectively, in code such as the following:
matrix[index].setFillColor(normalBackColor);
matrix[index].setTextColor(normalForeColor);
This code is from the SpreadsheetContainer constructor. The two
setFillColor() and setTextColor()
methods are also called when a cell is given the current highlight
focus. In this case, the cell is given the highlight colors. Some
code is repeated here from the SpreadsheetCell object's paint()
method to illustrate how these colors are applied to the cell:
// 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);
// Draw the string
g.setColor(textColor);
g.drawString(textPaint,x + 4,y + (height - 2));
The fillColor and textColor
variables are set by the setFillColor()
and setTextColor() methods,
respectively.
Four accessor methods are added to the SpreadsheetContainer class
for getting the normal and highlight Color variables. These are
called by the selectedChoiceListItem()
method of the ChooseColorDialog class to retrieve the colors
to be displayed in the colorDisplay object.
When you click the Update button, the color dialog box's setSpreadsheetColors()
is invoked to update the spreadsheet colors. This in turn calls
the SpreadsheetContainer's setNewColors()
method, which applies the four normal and highlight colors to
each of the SpreadsheetCell objects that are not row or column
headers. The container is then repainted, displaying the cells
in their new colors.
The discussion of the spreadsheet applet's font dialog box will
not be as lengthy as the preceeding overview of the color dialog.
In many ways, it is very similar, so a detailed explanation doesn't
need to be repeated; refer to this book's CD-ROM for any details
not covered in this overview.
Figure 4.11 shows the font dialog box. It is based on the ChooseFontDialog
class, which displays its components in a two-column style similar
to the color dialog box. The current font family, style, and size
is shown in a Canvas display object of the fontDisplay class.
This class is very similar to the colorDisplay class.
The list component on the left side of the dialog box shows the
fonts available on the current platform. It uses the AWT Toolkit
class to get this information. Here is the code that creates the
control and adds the font families:
// Add the listbox of choices...
// Get the selection from the toolkit...
choiceList = new List();
String fontList[] =
Toolkit.getDefaultToolkit().getFontList();
for (int i = 0; i < fontList.length;
++i)
choiceList.addItem(fontList[i]);
A choice box is added to the dialog box to enumerate font sizes
that can be used. Two checkboxes are used to set the bold and
italicized styles. If none of these are set, the font's style
is set to plain.
Every time one of these controls is changed, the font display
is updated with a new font. This happens in the paintSample()
method, whose text is found in Listing 4.13. The variable d
in the code represents the fontDisplay object.
Listing 4.13. The paintSample()
method for displaying a font with selected attributes.
// Set the display canvas to show itself
with
// the currently selected font
private synchronized void paintSample() {
// Get the family to display
String fontName = choiceList.getSelectedItem();
// Get its point size
String fontSize = choiceSize.getSelectedItem();
// Set its style
int fontStyle = Font.PLAIN;
if (checkItalics.getState())
fontStyle
+= Font.ITALIC;
if (checkBold.getState())
fontStyle
+= Font.BOLD;
// Create a font with the
proper attributes...
currentFont = new Font(fontName,fontStyle,
Integer.parseInt(fontSize));
// Set the new font on the
canvas...
d.setFont(currentFont);
// Repaint it so the new font
is displayed..
d.repaint();
}
When you select OK, the setFont()
method of the SpreadsheetContainer class is called; the font created
in the previous paintSample()
code is passed as the parameter. This setFont()
method overrides the default setFont()
method, as seen in Listing 4.14. The method goes through every
font in the container and sets it to the new font. The container
is then repainted to show the new font. Figure 4.12 shows what
the spreadsheet looks like when it is set with a Helvetica Bold
16-point font.
Figure 4.12 : The spreadsheet applet set to Helvetica Bold 16-point font.
Listing 4.14. The SpreadsheetContainer setFont()
method.
// Set the font for all the individual
// spreadsheet cells...
public synchronized void setFont(Font f) {
super.setFont(f);
int index;
// Reload the font for each cell...
for (int i = 0; i < numRows; ++i)
{
for (int j = 0; j < numColumns;
++j) {
index = (i * (numColumns))
+ j;
// Set the colors...
matrix[index].setFont(f);
} // end inner
for...
} // end outer for
// Repaint to show new font...
repaint();
}
The FileDialog class is a subclass of Dialog used to provide a
platform-independent approach to letting the user select what
files are to be loaded or saved. Instances of the FileDialog class
will usually mirror the underlying GUI conventions. For example,
a FontDialog object for loading a file on the Windows 95 environment
is shown in Figure 4.13. As you can see from the example, the
dialog box follows the Windows 95 Open dialog box conventions.
Figure 4.13 : The load file font Dialog.
The FileDialog can be constructed to be in a mode to load a file
or save a file. These dialog boxes look similar but perform slightly
differently. For example, the Save version of the dialog box will
notify the user that a file exists if a filename is given for
a preexisting file. The SpreadsheetFrame class constructs a dialog
box for both the load and save cases:
FileDialog openFileDialog; //
File Open Dialog...
FileDialog saveFileDialog; // File Save Dialog...
openFileDialog = new FileDialog(this,"Open File",
FileDialog.LOAD);
saveFileDialog = new FileDialog(this,"Save File",
FileDialog.SAVE);
The integer flag in the last parameter of the constructors indicates
the mode in which the dialog box should function.
When the dialog box is displayed with the show()
method, it acts in a modal fashion so that input to the frame
is blocked while it is up. After a choice has been made, the chosen
file and directory can be retrieved with the getFile()
and getDirectory() methods.
The former will return a null value if the user had canceled out
of the dialog box. If the names are valid, the SpreadsheetFrame
class either saves or opens the specified spreadsheet file.
It is interesting to note that the FileDialog objects constructed
here do not appear when the applet is run in Netscape Navigator.
Recall that Netscape does not allow any file-based methods to
be invoked from an applet. Consequently, there is no reason to
even display FileDialog objects.
The process of saving a spreadsheet file is surprisingly simple.
What is saved is not the contents of the SpreadsheetContainer
object, but the CellContainer object. This is because the former
has some useless information like row/column headers, but the
real data is in the CellContainer.
Listing 4.15 shows the CellContainer method for saving a file.
This code was easy to generate because of the FilterOutputStream
classes discussed in the overview of streams. The DataOutputStream
variable fs is used to hide
the complexities of the underlying BufferedOutputStream and FileOutputStream
objects. The former is used as a cache so that every stream write
does not result in a disk write. In short, BufferedOutputStream
is used to improve performance. The inner part of the constructor,
FileOutputStream, actually opens the file for writing. If there
is a problem opening or writing the file at any point in the process,
an IOException will be thrown. The saveFile()
method simply rethrows the exception, forcing the calling method
to catch the problem.
Listing 4.15. The CellContainer method for saving a file.
// Save the cell container to a file...
public void saveFile(String filename) throws
IOException {
// Perform these file operations. If
anything goes wrong
// an exception will be thrown. This
must be caught
// by the calling method...
// Open the file...
DataOutputStream fs = new
DataOutputStream(
new
BufferedOutputStream(
new
FileOutputStream(filename)));
// Write out magic number
fs.writeInt(magicNumber);
// Write out row and column
sizes
fs.writeInt(numRows);
fs.writeInt(numColumns);
// Now go through each row
and column and save...
// Write out literal value
followed by delimited...
int numCells = numRows * numColumns;
for (int i = 0; i < numCells;
++i) {
fs.writeBytes(matrix[i].getCellString());
fs.writeByte(delimiter);
} // end for
// Close the file. You're
done!
fs.close();
}
The first thing written to the file is a magic integer number.
This is used to indicate that the file is a spreadsheet file.
The method for opening a spreadsheet file (discussed in the next
section) will check for the existence of the magic number at the
beginning of the file. If it isn't there, an exception will be
thrown.
The saveFile() method writes
out the magic number and the number of rows and columns out as
integers, using the DataOutputStream writeInt()
method. The method then loops through each element in the CellContainer
matrix, writing out each cell's String contents followed by a
delimiter (in this case, a tab). Recall that the CellContainer
needs only the Cell's String value; the evaluated value can be
generated on the fly by calling the CellContainer reevaluate()
method.
The Cell string is written out through the writeBytes()
method. Why not use a writeString()
method? This does not exist because Java is a Unicode-based system.
This means that 8-bit ASCII character strings need to be treated
differently than 16-bit Unicode values. The writeBytes()
method is used here so that ASCII data can be written out.
Once a spreadsheet's contents have been saved to a file, it can
be reopened through the CellContainer's readFile()
method. This is detailed in Listing 4.16. Although it's similar
to the saveFile() method
in the previous section, it differs in some important ways. A
major difference is that the readFile()
method generates the Cell matrix of the CellContainer because
readFile() is called from
a new CellContainer constructor, which takes a filename as its
parameter. This constructor does little more than call readFile(),
which becomes responsible for setting up the Cell matrix.
Listing 4.16. The CellContainer method for reading in a file.
// Read in a file and create a new matrix
to
// accommodate the data...
public void readFile(String filename) throws
IOException {
// Set everything to empty...
numRows = 0;
numColumns = 0;
int magic;
byte b;
DataInputStream fs;
try {
// Open
the file...
fs = new
DataInputStream(new BufferedInputStream(
new
FileInputStream(filename)) );
}
// Can't open file, quit out...
catch (IOException e) {
throw e;
}
// Now try to load the data...
try {
//
Check the magic number
magic =
fs.readInt();
if
(magic != magicNumber)
throw
(new IOException("Invalid magic number"));
//
Get the rows and columns...
numRows
= fs.readInt();
numColumns
= fs.readInt();
if
((numRows <= 0) || (numColumns <=0) ||
(numRows
> maxRows) || (numColumns > maxColumns) )
throw
(new IOException("Invalid row/col settings"));
int
numCells = numRows * numColumns;
//
Allocate matrix...
matrix
= new Cell[numCells];
//
Now load the individual cells...
for
(int i = 0; i < numCells; ++i) {
//
Load the string up until delimited...
StringBuffer
s = new StringBuffer();
while
((b = fs.readByte()) != delimiter)
s.append((char)b);
//
Add it to new Cell...
matrix[i]
= new Cell();
matrix[i].setCellString(s);
}
// end for
//
Success! Close the file and leave...
fs.close();
}
// If exception, then close
file
// and reset everything...
catch (IOException e) {
numRows
= 0;
numColumns
= 0;
fs.close();
throw e;
}
}
Of course, the other difference between saveFile()
and readFile() is that the
latter works with an InputStream. The DataInputStream object fs
builds on BufferedInputStream and FileInputStream objects, the
latter of which actually opens the file for reading. The code
uses readInt() to check the
first few bytes of the file to make sure the magic number is present.
If it isn't, an exception is thrown. The readFile()
method throws an IOException if the magic number fails, the file
is not found, or there is a problem with the read. The constructor
does not catch thrown IOException objects, so it's up to the calling
object to catch IOException objects.
The individual Cell Strings are created through multiple calls
to readByte(). This is used
with the String append()
method to add byte characters to a new String until the delimiter
is found. When this happens, a new Cell is created with the String
and is assigned to the CellContainer matrix.
The SpreadsheetFrame object calls the CellContainer constructor
that loads a file when the load FileDialog object is used. It
takes the newly created CellContainer and sets it to the SpreadsheetContainer
with the newCellContainer()
method discussed in the previous chapter. This method uses the
loaded CellContainer object to generate new SpreadsheetCell objects.
It then repaints itself, displaying the new spreadsheet contents.
This chapter's tutorial provides several examples of how to use
Dialogs. In doing so, it also illustrates more principles for
using the Graphics object to paint colors and fonts, working with
layouts to create more complex displays, and learning the subtleties
of event handling. It also gives you an overview of Java streams
with examples of how to read and write to files. This latter capability
will be restricted depending on the browser environment (or lack
thereof) in which a Java applet is being executed.

Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.