The original design goal
of the graphical user interface (GUI) library in Java
1.0 was to allow the programmer to build a GUI that
looks good on all platforms.
That
goal was not achieved. Instead, the Java 1.0
Abstract
Window Toolkit (AWT) produces a GUI that looks equally mediocre on all
systems. In addition it’s restrictive: you can use only four fonts and you
cannot access any of the more sophisticated GUI elements that exist in your
operating system (OS). The Java 1.0 AWT programming model is also awkward and
non-object-oriented.
Much of this situation has been
improved with the Java 1.1 AWT event model, which takes
a much clearer, object-oriented approach, along with the introduction of Java
Beans, a component programming model that is particularly oriented toward the
easy creation of visual programming environments. Java
1.2 finishes the transformation away from the old Java
1.0 AWT by adding the Java
Foundation Classes (JFC), the GUI portion of which is called
“Swing.” These are a rich set of
easy-to-use, easy-to-understand Java Beans that can be dragged and dropped (as
well as hand programmed) to create a GUI that you can (finally) be satisfied
with. The “revision 3” rule of the software industry (a product
isn’t good until revision 3) seems to hold true with programming languages
as well.
One of Java’s primary design
goals is to create applets, which are little programs that run inside a
Web browser. Because they must be safe, applets are limited in what they can
accomplish. However, they are a powerful tool in supporting client-side
programming, a major issue for the Web.
Programming within an applet is so
restrictive that it’s often referred to as being “inside the
sandbox,” since you always have someone – the Java run-time security
system – watching over you. Java 1.1 offers
digital signing for applets so you can choose to allow trusted applets to have
access to your machine. However, you can also step outside the sandbox and write
regular applications, in which case you can access the other features of your
OS. We’ve been writing regular applications all along in this book, but
they’ve been console applications without any graphical components.
The AWT can also be used to build GUI interfaces for regular
applications.
In this chapter you’ll first
learn the use of the original “old” AWT, which is still supported
and used by many of the code examples that you will come across. Although
it’s a bit painful to learn the old AWT, it’s necessary because you
must read and maintain legacy code that uses the old AWT. Sometimes you’ll
even need to write old AWT code to support environments that haven’t
upgraded past Java 1.0. In the second part of the
chapter you’ll learn about the structure of the “new” AWT in
Java 1.1 and see how much better the event model is. (If
you can, you should use the newest tools when you’re creating new
programs.) Finally, you’ll learn about the new JFC/Swing components, which
can be added to Java 1.1 as a library – this means you can use the library
without requiring a full upgrade to Java 1.2.
Most of the examples will show the
creation of applets, not only because it’s easier but also because
that’s where the AWT’s primary usefulness might reside. In addition
you’ll see how things are different when you want to create a regular
application using the AWT, and how to create programs that are both applets and
applications so they can be run either inside a browser or from the command
line.
Please be aware that this is not a
comprehensive glossary of all the methods for the described classes. This
chapter will just get you started with the essentials. When you’re looking
for more sophistication, make sure you go to your information browser to look
for the classes and methods that will solve your problem. (If you’re using
a development environment your information browser might be built in; if
you’re using the Sun JDK then you use your Web browser and start in the
java root directory.) Appendix F lists other resources for learning library
details.
One of the problems with the
“old” AWT that you’ll learn about in this chapter is that it
is a poor example of both object-oriented design and GUI development kit design.
It throws us back into the dark ages of programming (some suggest that the
‘A’ in AWT stands for “awkward,” “awful,”
“abominable,” etc.). You must write lines of code to do
everything, including tasks that are accomplished much more easily using
resources in other environments.
So why learn to
use the old AWT? “Because it’s there.” In this case,
“there” has a much more ominous meaning and points to a tenet of
object-oriented library design: Once you publicize a component in your
library, you can never take it out. If you do, you’ll wreck
somebody’s existing code. In addition, there are many existing code
examples out there that you’ll read as you learn about Java and they all
use the old AWT.
The AWT must reach into the GUI
components of the native OS, which means that it performs a task that an applet
cannot otherwise accomplish. An untrusted applet cannot make any direct calls
into an OS because otherwise it could do bad things to the user’s machine.
The only way an untrusted applet can access important functionality such as
“draw a window on the screen” is through calls in the standard Java
library that’s been specially ported and safety checked for that machine.
The original model that Sun created is that this “trusted library”
will be provided only by the trusted vendor of the Java system in your Web
browser, and the vendor will control what goes into that library.
But what if you want to extend the
system by adding a new component that accesses functionality in the OS? Waiting
for Sun to decide that your extension should be incorporated into the standard
Java library isn’t going to solve your problem. The new model in Java 1.1
is “trusted code” or “signed code” whereby a special
server verifies that a piece of code that you download is in fact
“signed” by the stated author using a public-key encryption system.
This way, you’ll know for sure where the code comes from, that it’s
Bob’s code and not just someone pretending to be Bob. This doesn’t
prevent Bob from making mistakes or doing something malicious, but it does
prevent Bob from shirking responsibility – anonymity is what makes
computer viruses possible. A digitally signed applet – a “trusted
applet” – in Java 1.1 can reach into your machine and
manipulate it directly, just like any other application you get from a
“trusted” vendor and install onto your computer.
But the point of all this is that
the old AWT is there. There will always be old AWT code floating around
and new Java programmers learning from old books will encounter that code. Also,
the old AWT is worth studying as an example of poor library design. The coverage
of the old AWT given here will be relatively painless since it won’t go
into depth and enumerate every single method and class, but instead give you an
overview of the old AWT design.
Libraries are often grouped
according to their functionality. Some libraries, for example, are used as is,
off the shelf. The standard Java library String and Vector classes
are examples of these. Other libraries are designed specifically as building
blocks to build other classes. A certain class of library is the
application framework,
whose goal is to help you build applications by providing a class or set of
classes that produces the basic behavior that you need in every application of a
particular type. Then, to customize the behavior to your own needs you inherit
from the application class and override the methods of interest. The application
framework’s default control mechanism will call your overridden methods at
the appropriate time. An application framework is a good example of
“separating the things that change from the things that stay the
same,” since it attempts to localize all the unique parts of a program in
the overridden methods.
Applets are
built using an application framework. You inherit from class Applet and
override the appropriate methods. Most of the time you’ll be concerned
with only a few important methods that have to do with how the applet is built
and used on a Web page. These methods are:
Method |
Operation |
---|---|
init( ) |
Called when the applet is first
created to perform first-time initialization of the applet |
start( ) |
Called every time the applet moves
into sight on the Web browser to allow the applet to start up its normal
operations (especially those that are shut off by stop( )). Also
called after init( ). |
paint( ) |
Part of the base class
Component (three levels of inheritance up). Called as part of an
update( ) to perform special painting on the canvas of an
applet. |
stop( ) |
Called every time the applet moves
out of sight on the Web browser to allow the applet to shut off expensive
operations. Also called right before destroy( ). |
destroy( ) |
Called when the applet is being
unloaded from the page to perform final release of resources when the applet is
no longer used |
Consider the paint( )
method. This method is called automatically when the
Component (in this case,
the applet) decides that it needs to update itself – perhaps because
it’s being moved back onto the screen or placed on the screen for the
first time, or perhaps some other window had been temporarily placed over your
Web browser. The applet calls its
update( ) method
(defined in the base class Component), which goes about restoring
everything, and as a part of that restoration calls paint( ). You
don’t have to override paint( ), but it turns out to be an
easy way to make a simple applet, so we’ll start out with
paint( ).
When update( ) calls
paint( ) it hands it a handle to a
Graphics object that
represents the surface on which you can paint. This is important because
you’re limited to the surface of that particular component and thus cannot
paint outside that area, which is a good thing or else you’d be painting
outside the lines. In the case of an applet, the surface is the area inside the
applet.
The Graphics object also has
a set of operations you can perform on it. These operations revolve around
painting on the canvas, so most of them have to do with drawing images, shapes,
arcs, etc. (Note that you can look all this up in your online Java documentation
if you’re curious.) There are some methods that allow you to draw
characters, however, and the most commonly used one is
drawString( ). For
this, you must specify the String you want to draw and its starting
location on the applet’s drawing surface. This location is given in
pixels, so it will look different on different machines, but at least it’s
portable.
With this information you can
create a simple applet:
//: Applet1.java // Very simple applet package c13; import java.awt.*; import java.applet.*; public class Applet1 extends Applet { public void paint(Graphics g) { g.drawString("First applet", 10, 10); } } ///:~
Note that applets are not required
to have a main( ). That’s all wired in to the application
framework; you put any startup code in init( ).
To run this program you must place
it inside a Web page and view that page inside your Java-enabled Web browser. To
place an applet inside a Web
page you put a special tag inside the HTML source for that Web
page[51] to
tell the page how to load and run the applet. This is the applet
tag, and it looks like this for
Applet1:
<applet code=Applet1 width=200 height=200> </applet>
The code value gives the
name of the .class file where the applet resides. The width and
height specify the initial size of the applet (in pixels, as before).
There are other items you can place within the applet tag: a place to find other
.class files on the Internet
(codebase), alignment
information (align), a
special identifier that makes it possible for applets to communicate with each
other
(name),
and applet
parameters to provide
information that the applet can retrieve. Parameters are in the
form
<param name=identifier value = "information">
and there can be as many as you
want.
For simple applets all you need to
do is place an applet tag in the above form inside your Web page and that will
load and run the applet.
You can perform a simple test
without any network connection by starting up your Web browser and opening the
HTML file containing the applet tag. (Sun’s JDK also contains a tool
called the appletviewer that picks the
<APPLET> tags out of the HTML file and runs the applets without displaying
the surrounding HTML
text.[52]) As
the HTML file is loaded, the browser will discover the applet tag and go hunt
for the .class file specified by the code value. Of course, it
looks at the CLASSPATH to find out where to hunt, and if your .class file
isn’t in the CLASSPATH then it will give an error message on the status
line of the browser to the effect that it couldn’t find that .class
file.
When you want to try this out on
your Web site things are a little more complicated. First of all, you must
have a Web site, which for most people means a third-party
Internet Service Provider (ISP)
at a remote location. Then you must have a way to move the HTML files and the
.class files from your site to the correct directory (your WWW directory)
on the ISP machine. This is typically done with a
File Transfer Protocol (FTP)
program, of which there are many different types freely available. So it would
seem that all you need to do is move the files to the ISP machine with FTP, then
connect to the site and HTML file using your browser; if the applet comes up and
works, then everything checks out, right?
Here’s where you can get
fooled. If the browser cannot locate the .class file on the server, it
will hunt through the CLASSPATH
on your local machine. Thus, the applet might not be loading properly
from the server, but to you it looks fine because the browser finds it on your
machine. When someone else logs in, however, his or her browser can’t find
it. So when you’re testing, make sure you erase the relevant .class
files on your machine to be safe.
One of the most insidious places
where this happened to me is when I innocently placed an applet inside a
package. After uploading the HTML file and applet, it turned out that the
server path to the applet was confused because of the package name. However, my
browser found it in the local CLASSPATH. So I was the only one who could
properly load the applet. It took some time to discover that the package
statement was the culprit. In general, you’ll want to leave the
package statement out of
an applet.
The example above isn’t too
thrilling, so let’s try adding a slightly more interesting graphic
component:
//: Applet2.java // Easy graphics import java.awt.*; import java.applet.*; public class Applet2 extends Applet { public void paint(Graphics g) { g.drawString("Second applet", 10, 15); g.draw3DRect(0, 0, 100, 20, true); } } ///:~
This puts a box around the string.
Of course, all the numbers are hard-coded and are based on pixels, so on some
machines the box will fit nicely around the string and on others it will
probably be off, because fonts will be different on different
machines.
There are other interesting things
you can find in the documentation for the Graphic class. Any sort of
graphics activity is usually entertaining, so further experiments of this sort
are left to the reader.
It’s interesting to see some
of the framework methods in action. (This example will look only at
init( ),
start( ), and
stop( ) because
paint( ) and destroy( ) are self-evident and not so
easily traceable.) The following applet keeps track of the number of times these
methods are called and displays them using
paint( ):
//: Applet3.java // Shows init(), start() and stop() activities import java.awt.*; import java.applet.*; public class Applet3 extends Applet { String s; int inits = 0; int starts = 0; int stops = 0; public void init() { inits++; } public void start() { starts++; } public void stop() { stops++; } public void paint(Graphics g) { s = "inits: " + inits + ", starts: " + starts + ", stops: " + stops; g.drawString(s, 10, 10); } } ///:~
Normally when you override a method
you’ll want to look to see whether you need to call the base-class version
of that method, in case it does something important. For example, with
init( ) you might need to call super.init( ). However,
the Applet documentation specifically states that the
init( ), start( ), and stop( ) methods in
Applet do nothing, so it’s not necessary to call them
here.
When you experiment with this
applet you’ll discover that if you minimize the Web browser or cover it up
with another window you might not get calls to stop( ) and
start( ). (This behavior seems to vary among implementations;
you might wish to contrast the behavior of Web browsers with that of applet
viewers.) The only time the calls will occur is when you move to a different Web
page and then come back to the one containing the
applet.
Making a button is quite simple:
you just call the Button
constructor with the label you want on the button. (You can also use the default
constructor if you want a button with no label, but this is not very useful.)
Usually you’ll want to create a handle for the button so you can refer to
it later.
The Button is a component,
like its own little window, that will automatically get repainted as part of an
update. This means that you don’t explicitly paint a button or any other
kind of control; you simply place them on the form and let them automatically
take care of painting themselves. So to place a button on a form you override
init( ) instead of overriding paint( ):
//: Button1.java // Putting buttons on an applet import java.awt.*; import java.applet.*; public class Button1 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } } ///:~
It’s not enough to create the
Button (or any other control). You must also call the Applet
add( ) method to cause the button to be placed on the applet’s
form. This seems a lot simpler than it is, because the call to
add( ) actually decides, implicitly, where to place the control on
the form. Controlling the layout of a form is examined
shortly.
You’ll notice that if you
compile and run the applet above, nothing happens when you press the buttons.
This is where you must step in and write some code to determine what will
happen. The basis of
event-driven
programming, which comprises a lot of what a GUI is about, is tying events to
code that responds to those events.
After working your way this far
through the book and grasping some of the fundamentals of object-oriented
programming, you might think that of course there will be some sort of
object-oriented approach to handling events. For example, you might have to
inherit each button and override some “button pressed” method (this,
it turns out, is too tedious and restrictive). You might also think
there’s some master “event” class that contains a method for
each event you want to respond to.
Before objects, the typical
approach to handling events was the “giant switch statement.” Each
event would have a unique integer value and inside the master event handling
method you’d write a switch on that value.
The AWT in Java
1.0 doesn’t use any object-oriented approach.
Neither does it use a giant switch statement that relies on the
assignment of numbers to events. Instead, you must create a cascaded set of
if statements. What you’re trying to do with the if
statements is detect the object that was the
target of the event. That
is, if you click on a button, then that particular button is the target.
Normally, that’s all you care about – if a button is the target of
an event, then it was most certainly a mouse click and you can continue based on
that assumption. However, events can contain other information as well. For
example, if you want to find out the pixel location where a mouse click occurred
so you can draw a line to that location, the Event object will contain
the location. (You should also be aware that Java 1.0 components can be limited
in the kinds of events they generate, while Java 1.1 and Swing/JFC components
produce a full set of events.)
The Java
1.0 AWT method where your cascaded if statement
resides is called
action( ). Although
the whole Java 1.0 Event model has been deprecated in
Java 1.1, it is still widely used for simple applets and
in systems that do not yet support Java 1.1, so I recommend you become
comfortable with it, including the use of the following action() method
approach.
action( ) has two
arguments: the first is of type
Event and contains all
the information about the event that triggered this call to
action( ). For example, it could be a mouse click, a normal keyboard
press or release, a special key press or release, the fact that the component
got or lost the focus, mouse movements, or drags, etc. The second argument is
usually the target of the event, which you’ll often ignore. The second
argument is also encapsulated in the Event object so it is redundant as
an argument.
The situations in which
action( ) gets called are extremely limited: When you place controls
on a form, some types of controls (buttons, check boxes, drop-down lists, menus)
have a “standard action” that occurs, which causes the call to
action( ) with the appropriate Event object. For example,
with a button the action( ) method is called when the button is
pressed and at no other time. Usually this is just fine, since that’s what
you ordinarily look for with a button. However, it’s possible to deal with
many other types of events via the
handleEvent( )
method as we will see later in this chapter.
The previous example can be
extended to handle button clicks as follows:
//: Button2.java // Capturing button presses import java.awt.*; import java.applet.*; public class Button2 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } public boolean action(Event evt, Object arg) { if(evt.target.equals(b1)) getAppletContext().showStatus("Button 1"); else if(evt.target.equals(b2)) getAppletContext().showStatus("Button 2"); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
To see what the
target is, ask the Event
object what its target member is and then use the
equals( ) method to see if it matches the
target object handle you’re interested in. When you’ve written
handlers for all the objects you’re interested in you must call
super.action(evt, arg) in
the else statement at the end, as shown above. Remember from Chapter 7
(polymorphism) that your overridden method is called instead of the base class
version. However, the base-class version contains code to handle all of the
cases that you’re not interested in, and it won’t get called unless
you call it explicitly. The return value indicates whether you’ve handled
it or not, so if you do match an event you should return true, otherwise
return whatever the base-class event( ) returns.
For this example, the simplest
action is to print what button is pressed. Some systems allow you to pop up a
little window with a message in it, but applets discourage this. However, you
can put a message at the bottom of the
Web
browser window on its status line by calling the Applet method
getAppletContext( )
to get access to the browser and then
showStatus( ) to put
a string on the status
line.[53] You
can print out a complete description of an event the same way, with
getAppletContext().showStatus(evt + "" ). (The empty String forces
the compiler to convert evt to a String.) Both of these reports
are really useful only for testing and debugging since the browser might
overwrite your message.
Strange as it might seem, you can
also match an event to the
text that’s on a button through the second argument in
event( ). Using this technique, the example above
becomes:
//: Button3.java // Matching events on button text import java.awt.*; import java.applet.*; public class Button3 extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { add(b1); add(b2); } public boolean action (Event evt, Object arg) { if(arg.equals("Button 1")) getAppletContext().showStatus("Button 1"); else if(arg.equals("Button 2")) getAppletContext().showStatus("Button 2"); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
It’s difficult to know
exactly what the equals( ) method is doing here. The biggest problem
with this approach is that most new Java programmers who start with this
technique spend at least one frustrating session discovering that they’ve
gotten the capitalization or spelling wrong when comparing to the text on a
button. (I had this experience.) Also, if you change the text of the button, the
code will no longer work (but you won’t get any compile-time or run-time
error messages). You should avoid this approach if
possible.
A
TextField is a one line
area that allows the user to enter and edit text. TextField is inherited
from TextComponent,
which lets you select text, get the selected text as a String, get or
set the text, and set whether the TextField is editable, along with other
associated methods that you can find in your online reference. The following
example demonstrates some of the functionality of a TextField; you can
see that the method names are fairly obvious:
//: TextField1.java // Using the text field control import java.awt.*; import java.applet.*; public class TextField1 extends Applet { Button b1 = new Button("Get Text"), b2 = new Button("Set Text"); TextField t = new TextField("Starting text", 30); String s = new String(); public void init() { add(b1); add(b2); add(t); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) { getAppletContext().showStatus(t.getText()); s = t.getSelectedText(); if(s.length() == 0) s = t.getText(); t.setEditable(true); } else if(evt.target.equals(b2)) { t.setText("Inserted by Button 2: " + s); t.setEditable(false); } // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
There are several ways to construct
a TextField; the one shown here provides an initial string and sets the
size of the field in characters.
Pressing button 1 either gets the
text you’ve selected with the mouse or it gets all the text in the field
and places the result in String s. It also allows the field to be edited.
Pressing button 2 puts a message and s into the text field and prevents
the field from being edited (although you can still select the text). The
editability of the text is controlled by passing
setEditable( ) a
true or
false.
A
TextArea is like a
TextField except that it can have multiple lines and has significantly
more functionality. In addition to what you can do with a TextField, you
can append text and insert or replace text at a given location. It seems like
this functionality could be useful for TextField as well, so it’s a
little confusing to try to detect how the distinction is made. You might think
that if you want TextArea functionality everywhere you can simply use a
one line TextArea in places where you would otherwise use a
TextField. In Java 1.0, you also got scroll bars
with a TextArea even when they weren’t appropriate; that is, you
got both vertical and horizontal scroll bars for a one line TextArea. In
Java 1.1 this was remedied with an extra constructor
that allows you to select which scroll bars (if any) are present. The following
example shows only the Java 1.0 behavior, in which the
scrollbars are always on. Later in the chapter you’ll see an example that
demonstrates Java 1.1 TextAreas.
//: TextArea1.java // Using the text area control import java.awt.*; import java.applet.*; public class TextArea1 extends Applet { Button b1 = new Button("Text Area 1"); Button b2 = new Button("Text Area 2"); Button b3 = new Button("Replace Text"); Button b4 = new Button("Insert Text"); TextArea t1 = new TextArea("t1", 1, 30); TextArea t2 = new TextArea("t2", 4, 30); public void init() { add(b1); add(t1); add(b2); add(t2); add(b3); add(b4); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) getAppletContext().showStatus(t1.getText()); else if(evt.target.equals(b2)) { t2.setText("Inserted by Button 2"); t2.appendText(": " + t1.getText()); getAppletContext().showStatus(t2.getText()); } else if(evt.target.equals(b3)) { String s = " Replacement "; t2.replaceText(s, 3, 3 + s.length()); } else if(evt.target.equals(b4)) t2.insertText(" Inserted ", 10); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } } ///:~
There are several different
TextArea constructors, but the one shown here gives a starting string and
the number of rows and columns. The different buttons show getting, appending,
replacing, and inserting
text.
A
Label does exactly what
it sounds like it should: places a label on the form. This is particularly
important for text fields and text areas that don’t have labels of their
own, and can also be useful if you simply want to place textual information on a
form. You can, as shown in the first example in this chapter, use
drawString( ) inside paint( ) to place text in an exact
location. When you use a Label it allows you to (approximately) associate
the text with some other component via the layout manager (which will be
discussed later in this chapter).
With the constructor you can create
a blank label, a label with initial text in it (which is what you’ll
typically do), and a label with an alignment of CENTER, LEFT, or
RIGHT (static final ints defined in class Label). You can
also change the label and its alignment with
setText( ) and
setAlignment( ), and
if you’ve forgotten what you’ve set these to you can read the values
with getText( ) and
getAlignment( ).
This example shows what you can do with labels:
//: Label1.java // Using labels import java.awt.*; import java.applet.*; public class Label1 extends Applet { TextField t1 = new TextField("t1", 10); Label labl1 = new Label("TextField t1"); Label labl2 = new Label(" "); Label labl3 = new Label(" ", Label.RIGHT); Button b1 = new Button("Test 1"); Button b2 = new Button("Test 2"); public void init() { add(labl1); add(t1); add(b1); add(labl2); add(b2); add(labl3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(b1)) labl2.setText("Text set into Label"); else if(evt.target.equals(b2)) { if(labl3.getText().trim().length() == 0) labl3.setText("labl3"); if(labl3.getAlignment() == Label.LEFT) labl3.setAlignment(Label.CENTER); else if(labl3.getAlignment()==Label.CENTER) labl3.setAlignment(Label.RIGHT); else if(labl3.getAlignment() == Label.RIGHT) labl3.setAlignment(Label.LEFT); } else return super.action(evt, arg); return true; } } ///:~
The first use of the label is the
most typical: labeling a TextField or TextArea. In the second part
of the example, a bunch of empty spaces are reserved and when you press the
“Test 1” button setText( ) is used to insert text into
the field. Because a number of blank spaces do not equal the same number of
characters (in a
proportionally-spaced
font) you’ll see that the text gets truncated when inserted into the
label.
The third part of the example
reserves empty space, then the first time you press the “Test 2”
button it sees that there are no characters in the label (since
trim( ) removes all of the blank spaces at
each end of a String) and inserts a short label, which is initially
left-aligned. The rest of the times you press the button it changes the
alignment so you can see the effect.
You might think that you could
create an empty label and then later put text in it with setText( ).
However, you cannot put text into an empty label – presumably because it
has zero width – so creating a label with no text seems to be a useless
thing to do. In the example above, the “blank” label is filled with
empty spaces so it has enough width to hold text that’s placed inside
later.
Similarly, setAlignment( )
has no effect on a label that you’d typically create with text in the
constructor. The label width is the width of the text, so changing the alignment
doesn’t do anything. However, if you start with a long label and then
change it to a shorter one you can see the effect of the
alignment.
These behaviors occur because of
the default layout
manager that’s used for applets, which causes things to be squished
together to their smallest size. Layout managers will be covered later in this
chapter, when you’ll see that other layouts don’t have the same
effect.
A check box provides a way to make
a single on-off choice; it consists of a tiny box and a label. The box typically
holds a little ‘x’ (or some other indication that it is set) or is
empty depending on whether that item was selected.
You’ll normally create a
Checkbox using a
constructor that takes the label as an argument. You can get and set the state,
and also get and set the label if you want to read or change it after the
Checkbox has been created. Note that the capitalization of
Checkbox is inconsistent with the other controls, which could catch you
by surprise since you might expect it to be
“CheckBox.”
Whenever a Checkbox is set
or cleared an event occurs, which you can capture the same way you do a button.
The following example uses a TextArea to enumerate all the check boxes
that have been checked:
//: CheckBox1.java // Using check boxes import java.awt.*; import java.applet.*; public class CheckBox1 extends Applet { TextArea t = new TextArea(6, 20); Checkbox cb1 = new Checkbox("Check Box 1"); Checkbox cb2 = new Checkbox("Check Box 2"); Checkbox cb3 = new Checkbox("Check Box 3"); public void init() { add(t); add(cb1); add(cb2); add(cb3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(cb1)) trace("1", cb1.getState()); else if(evt.target.equals(cb2)) trace("2", cb2.getState()); else if(evt.target.equals(cb3)) trace("3", cb3.getState()); else return super.action(evt, arg); return true; } void trace(String b, boolean state) { if(state) t.appendText("Box " + b + " Set\n"); else t.appendText("Box " + b + " Cleared\n"); } } ///:~
The trace( ) method
sends the name of the selected Checkbox and its current state to the
TextArea using
appendText( ) so
you’ll see a cumulative list of the checkboxes that were selected and what
their state
is.
The concept of a
radio
button in GUI programming comes from pre-electronic car radios with mechanical
buttons: when you push one in, any other button that was pressed pops out. Thus
it allows you to force a single choice among many.
The AWT does not have a separate
class to represent the radio button; instead it reuses the
Checkbox. However, to put
the Checkbox in a radio button group (and to change its shape so
it’s visually different from an ordinary Checkbox) you must use a
special constructor that takes a
CheckboxGroup object as
an argument. (You can also call
setCheckboxGroup( )
after the Checkbox has been created.)
A CheckboxGroup has no
constructor argument; its sole reason for existence is to collect some
Checkboxes into a group of radio buttons. One of the Checkbox
objects must have its state set to true before you try to display the
group of radio buttons; otherwise you’ll get an exception at run time. If
you try to set more than one radio button to true then only the final one
set will be true.
Here’s a simple example of
the use of radio buttons. Note that you capture radio button events like all
others:
//: RadioButton1.java // Using radio buttons import java.awt.*; import java.applet.*; public class RadioButton1 extends Applet { TextField t = new TextField("Radio button 2", 30); CheckboxGroup g = new CheckboxGroup(); Checkbox cb1 = new Checkbox("one", g, false), cb2 = new Checkbox("two", g, true), cb3 = new Checkbox("three", g, false); public void init() { t.setEditable(false); add(t); add(cb1); add(cb2); add(cb3); } public boolean action (Event evt, Object arg) { if(evt.target.equals(cb1)) t.setText("Radio button 1"); else if(evt.target.equals(cb2)) t.setText("Radio button 2"); else if(evt.target.equals(cb3)) t.setText("Radio button 3"); else return super.action(evt, arg); return true; } } ///:~
To display the state, an text field
is used. This field is set to non-editable because it’s used only to
display data, not to collect it. This is shown as an alternative to using a
Label. Notice the text in the field is initialized to “Radio button
2” since that’s the initial selected radio button.
Like a group of radio buttons, a
drop-down
list is a way to force the user to select only one element from a group of
possibilities. However, it’s a much more compact way to accomplish this,
and it’s easier to change the elements of the list without surprising the
user. (You can change radio buttons dynamically, but that tends to be visibly
jarring).
Java’s
Choice box is not like
the combo box in Windows, which lets you select from a list or type in
your own selection. With a Choice box you choose one and only one element
from the list. In the following example, the Choice box starts with a
certain number of entries and then new entries are added to the box when a
button is pressed. This allows you to see some interesting behaviors in
Choice boxes:
//: Choice1.java // Using drop-down lists import java.awt.*; import java.applet.*; public class Choice1 extends Applet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; TextField t = new TextField(30); Choice c = new Choice(); Button b = new Button("Add items"); int count = 0; public void init() { t.setEditable(false); for(int i = 0; i < 4; i++) c.addItem(description[count++]); add(t); add(c); add(b); } public boolean action (Event evt, Object arg) { if(evt.target.equals(c)) t.setText("index: " + c.getSelectedIndex() + " " + (String)arg); else if(evt.target.equals(b)) { if(count < description.length) c.addItem(description[count++]); } else return super.action(evt, arg); return true; } } ///:~
The TextField displays the
“selected index,” which is the sequence number of the currently
selected element, as well as the String representation of the second
argument of action( ), which is in this case the string that was
selected.
When you run this applet, pay
attention to the determination of the size of the Choice box: in Windows,
the size is fixed from the first time you drop down the list. This means that if
you drop down the list, then add more elements to the list, the elements will be
there but the drop-down list won’t get any
longer[54]
(you can scroll through the elements). However, if you add all the elements
before the first time the list is dropped down, then it will be sized correctly.
Of course, the user will expect to see the whole list when it’s dropped
down, so this behavior puts some significant limitations on adding elements to
Choice
boxes.
List boxes are significantly
different from Choice boxes, and not just in appearance. While a
Choice box drops down when you activate it, a
List occupies some fixed
number of lines on a screen all the time and doesn’t change. In addition,
a List allows multiple selection: if you click on more than one item the
original item stays highlighted and you can select as many as you want. If you
want to see the items in a list, you simply call
getSelectedItems( ),
which produces an array of String of the items that have been
selected. To remove an item from a group you have to click it
again.
A problem with a List is
that the default action is double clicking, not single clicking. A single click
adds or removes elements from the selected group and a double click calls
action( ). One way around this is to re-educate your user, which is
the assumption made in the following program:
//: List1.java // Using lists with action() import java.awt.*; import java.applet.*; public class List1 extends Applet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; // Show 6 items, allow multiple selection: List lst = new List(6, true); TextArea t = new TextArea(flavors.length, 30); Button b = new Button("test"); int count = 0; public void init() { t.setEditable(false); for(int i = 0; i < 4; i++) lst.addItem(flavors[count++]); add(t); add(lst); add(b); } public boolean action (Event evt, Object arg) { if(evt.target.equals(lst)) { t.setText(""); String[] items = lst.getSelectedItems(); for(int i = 0; i < items.length; i++) t.appendText(items[i] + "\n"); } else if(evt.target.equals(b)) { if(count < flavors.length) lst.addItem(flavors[count++], 0); } else return super.action(evt, arg); return true; } } ///:~
When you press the button it adds
items to the top of the list (because of the second argument 0 to
addItem( )). Adding elements to a List is more reasonable
than the Choice box because users expect to scroll a list box (for one
thing, it has a built-in scroll bar) but they don’t expect to have to
figure out how to get a drop-down list to scroll, as in the previous
example.
However, the only way for
action( ) to be called is through a double-click. If you need to
monitor other activities that the user is doing on your List (in
particular, single clicks) you must take an alternative
approach.
So far we’ve been using
action( ), but there’s another method that gets first crack at
everything:
handleEvent( ). Any
time an event happens, it happens “over” or “to” a
particular object. The handleEvent( ) method for that object is
automatically called and an
Event object is created
and passed to handleEvent( ). The default handleEvent( )
(which is defined in
Component, the base class
for virtually all the “controls” in the AWT) will call either
action( ), as
we’ve been using, or other similar methods to indicate mouse activity,
keyboard activity, or to indicate that the focus has moved. We’ll look at
those later in this chapter.
What if these other methods –
action( ) in particular – don’t satisfy your needs? In
the case of List, for example, what if you want to catch single mouse
clicks but action( ) responds to only double clicks? The solution is
to override handleEvent( ) for your applet, which after all is
derived from Applet and can therefore override any non-final
methods. When you override handleEvent( ) for the applet
you’re getting all the applet events before they are routed, so you cannot
just assume “This has to do with my button so I can assume it’s been
pressed,” since that’s true only for action( ). Inside
handleEvent( ) it’s possible that the button has the focus and
someone is typing to it. Whether it makes sense or not, those are events that
you can detect and act upon in handleEvent( ).
To modify the List example
so that it will react to single mouse clicks, the button detection will be left
in action( ) but the code to handle the List will be moved
into handleEvent( ) as follows:
//: List2.java // Using lists with handleEvent() import java.awt.*; import java.applet.*; public class List2 extends Applet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; // Show 6 items, allow multiple selection: List lst = new List(6, true); TextArea t = new TextArea(flavors.length, 30); Button b = new Button("test"); int count = 0; public void init() { t.setEditable(false); for(int i = 0; i < 4; i++) lst.addItem(flavors[count++]); add(t); add(lst); add(b); } public boolean handleEvent(Event evt) { if(evt.id == Event.LIST_SELECT || evt.id == Event.LIST_DESELECT) { if(evt.target.equals(lst)) { t.setText(""); String[] items = lst.getSelectedItems(); for(int i = 0; i < items.length; i++) t.appendText(items[i] + "\n"); } else return super.handleEvent(evt); } else return super.handleEvent(evt); return true; } public boolean action(Event evt, Object arg) { if(evt.target.equals(b)) { if(count < flavors.length) lst.addItem(flavors[count++], 0); } else return super.action(evt, arg); return true; } } ///:~
The example is the same as before
except for the addition of handleEvent( ). Inside, a check is made
to see whether a list selection or deselection has occurred. Now remember,
handleEvent( ) is being overridden for the applet, so this
occurrence could be anywhere on the form and it could be happening to another
list. Thus, you must also check to see what the target is. (Although in this
case there’s only one list on the applet so we could have made the
assumption that all list events must be about that list. This is bad practice
since it’s going to be a problem as soon as another list is added.) If the
list matches the one we’re interested in, the same code as before will do
the trick.
Note that the form for
handleEvent( ) is similar to action( ): if you
deal with a particular event you return true, but if you’re
not interested in any of the other events via handleEvent( ) you
must return
super.handleEvent(evt). This
is vital because if you don’t do this, none of the other event-handling
code will get called. For example, try commenting out the return
super.handleEvent(evt) in the code above. You’ll discover that
action( ) never gets called, certainly not what you want. For both
action( ) and handleEvent( ) it’s important to
follow the format above and always return the base-class version of the method
when you do not handle the event yourself (in which case you should return
true). (Fortunately, these kinds of bug-prone details are relegated to
Java 1.0. The new design in Java
1.1 that you will see later in the chapter eliminates
these kinds of issues.)
In Windows, a list box
automatically allows
multiple
selections if you hold down the shift key. This is nice because it allows the
user to choose a single or multiple selection rather than fixing it during
programming. You might think you’ll be clever and implement this yourself
by checking to see if the shift key is held down when a mouse click was made by
testing for evt.shiftDown( ). Alas, the design of the AWT stymies
you – you’d have to be able to know which item was clicked on if the
shift key wasn’t pressed so you could deselect all the rest and
select only that one. However, you cannot figure that out in Java
1.0. (Java 1.1 sends all mouse,
keyboard, and focus events to a List, so you’ll be able to
accomplish
this.)
The way that you place components
on a form in Java is probably different from any other GUI system you’ve
used. First, it’s all code; there are no “resources” that
control placement of components. Second, the way components are placed on a form
is controlled by a “layout manager” that
decides how the components lie based on the order that you
add( ) them. The
size, shape, and placement of components will be remarkably different from one
layout manager to another. In addition, the layout managers adapt to the
dimensions of your applet or application window, so if that window dimension is
changed (for example, in the HTML page’s applet specification) the size,
shape, and placement of the components could change.
Both the
Applet and
Frame classes are derived
from Container, whose job
it is to contain and display Components. (The Container is a
Component so it can also
react to events.) In Container, there’s a method called
setLayout( ) that
allows you to choose a different layout manager.
In this section we’ll explore
the various layout managers by placing buttons in them (since that’s the
simplest thing to do). There won’t be any capturing of button events since
this is just intended to show how the buttons are laid
out.
So far, all the applets that have
been created seem to have laid out their components using some mysterious
internal logic. That’s because the applet uses a default layout scheme:
the FlowLayout. This
simply “flows” the components onto the form, from left to right
until the top space is full, then moves down a row and continues flowing the
components.
Here’s an example that
explicitly (redundantly) sets the layout manager in an applet to
FlowLayout and then places buttons on the form. You’ll notice that
with FlowLayout the components take on their “natural” size.
A Button, for example,
will be the size of its string.
//: FlowLayout1.java // Demonstrating the FlowLayout import java.awt.*; import java.applet.*; public class FlowLayout1 extends Applet { public void init() { setLayout(new FlowLayout()); for(int i = 0; i < 20; i++) add(new Button("Button " + i)); } } ///:~
All components will be compacted to
their smallest size in a FlowLayout, so you might get a little bit of
surprising behavior. For example, a label will be the size of its string, so
right-justifying it yields an unchanged
display.
This layout manager has the concept
of four border regions and a center area. When you add something to a panel
that’s using a
BorderLayout you must use
an add( ) method that takes a String object as its first
argument, and that string must specify (with proper capitalization)
“North” (top),
“South” (bottom),
“East” (right),
“West” (left), or
“Center.” If you misspell or mis-capitalize, you won’t get a
compile-time error, but the applet simply won’t do what you expect.
Fortunately, as you will see shortly, there’s a much-improved approach in
Java 1.1.
Here’s a simple
example:
//: BorderLayout1.java // Demonstrating the BorderLayout import java.awt.*; import java.applet.*; public class BorderLayout1 extends Applet { public void init() { int i = 0; setLayout(new BorderLayout()); add("North", new Button("Button " + i++)); add("South", new Button("Button " + i++)); add("East", new Button("Button " + i++)); add("West", new Button("Button " + i++)); add("Center", new Button("Button " + i++)); } } ///:~
For every placement but
“Center,” the element that you add is compressed to fit in the
smallest amount of space along one dimension while it is stretched to the
maximum along the other dimension. “Center,” however, spreads out
along both dimensions to occupy the middle.
A
GridLayout allows you to
build a table of components, and as you add them they are placed left-to-right
and top-to-bottom in the grid. In the constructor you specify the number of rows
and columns that you need and these are laid out in equal
proportions.
//: GridLayout1.java // Demonstrating the GridLayout import java.awt.*; import java.applet.*; public class GridLayout1 extends Applet { public void init() { setLayout(new GridLayout(7,3)); for(int i = 0; i < 20; i++) add(new Button("Button " + i)); } } ///:~
In this case there are 21 slots but
only 20 buttons. The last slot is left empty; no “balancing” goes on
with a
GridLayout.
The
CardLayout allows you to
create the rough equivalent of a “tabbed dialog,” which in more
sophisticated environments has actual file-folder tabs running across one edge,
and all you have to do is press a tab to bring forward a different dialog. Not
so in the AWT: The CardLayout is simply a blank space and you’re
responsible for bringing forward new cards. (The JFC/Swing library contains
tabbed panes that look much better and take care of all the details for
you.)
This example will combine more than
one layout type, which seems rather difficult at first since only one layout
manager can be operating for an applet or application. This is true, but if you
create more Panel
objects, each one of those Panels can have its own layout manager and
then be integrated into the applet or application as simply another component,
using the applet or application’s layout manager. This gives you much
greater flexibility as seen in the following example:
//: CardLayout1.java // Demonstrating the CardLayout import java.awt.*; import java.applet.Applet; class ButtonPanel extends Panel { ButtonPanel(String id) { setLayout(new BorderLayout()); add("Center", new Button(id)); } } public class CardLayout1 extends Applet { Button first = new Button("First"), second = new Button("Second"), third = new Button("Third"); Panel cards = new Panel(); CardLayout cl = new CardLayout(); public void init() { setLayout(new BorderLayout()); Panel p = new Panel(); p.setLayout(new FlowLayout()); p.add(first); p.add(second); p.add(third); add("North", p); cards.setLayout(cl); cards.add("First card", new ButtonPanel("The first one")); cards.add("Second card", new ButtonPanel("The second one")); cards.add("Third card", new ButtonPanel("The third one")); add("Center", cards); } public boolean action(Event evt, Object arg) { if (evt.target.equals(first)) { cl.first(cards); } else if (evt.target.equals(second)) { cl.first(cards); cl.next(cards); } else if (evt.target.equals(third)) { cl.last(cards); } else return super.action(evt, arg); return true; } } ///:~
This example begins by creating a
new kind of Panel: a ButtonPanel. This contains a single button,
placed at the center of a BorderLayout, which means that it will expand
to fill the entire panel. The label on the button will let you know which panel
you’re on in the CardLayout.
In the applet, both the Panel
cards where the cards will live and the layout manager cl for the
CardLayout must be members of the class because you need to have access
to those handles when you want to manipulate the cards.
The applet is changed to use a
BorderLayout instead of its default FlowLayout, a Panel is
created to hold three buttons (using a FlowLayout), and this panel is
placed at the “North” end of the applet. The cards panel is
added to the “Center” of the applet, effectively occupying the rest
of the real estate.
When you add the
ButtonPanels (or whatever other components you want) to the panel of
cards, the add( ) method’s first argument is not
“North,” “South,” etc. Instead, it’s a string that
describes the card. Although this string doesn’t show up anywhere on the
card, you can use it if you want to flip that card using the string. This
approach is not used in action( ); instead the first( ),
next( ), and last( ) methods are used. Check your
documentation for the other approach.
In Java, the use of some sort of
“tabbed panel” mechanism is quite important because (as you’ll
see later) in applet programming the use of pop-up dialogs is heavily
discouraged. For Java 1.0 applets, the CardLayout
is the only viable way for the applet to have a number of different forms that
“pop up” on
command.
Some time ago, it was believed that
all the stars, planets, the sun, and the moon revolved around the earth. It
seemed intuitive from observation. But then astronomers became more
sophisticated and started tracking the motion of individual objects, some of
which seemed at times to go backward in their paths. Since it was known that
everything revolved around the earth, those astronomers spent large amounts of
time coming up with equations and theories to explain the motion of the stellar
objects.
When trying to work with
GridBagLayout, you
can consider yourself the analog of one of those early astronomers. The basic
precept (decreed, interestingly enough, by the designers at “Sun”)
is that everything should be done in code. The Copernican revolution (again
dripping with irony, the discovery that the planets in the solar system revolve
around the sun) is the use of resources to determine the layout and make
the programmer’s job easy. Until these are added to Java, you’re
stuck (to continue the metaphor) in the Spanish Inquisition of
GridBagLayout and GridBagConstraints.
My recommendation is to avoid
GridBagLayout. Instead, use the other layout managers and especially the
technique of combining several panels using different layout managers within a
single program. Your applets won’t look that different; at least
not enough to justify the trouble that GridBagLayout entails. For my
part, it’s just too painful to come up with an example for this (and I
wouldn’t want to encourage this kind of library design). Instead,
I’ll refer you to Core Java by Cornell & Horstmann
(2nd ed., Prentice-Hall, 1997) to get
started.
As noted previously,
action( ) isn’t the only method that’s automatically
called by
handleEvent( ) once
it sorts everything out for you. There are three other sets of methods that are
called, and if you want to capture certain types of events (keyboard, mouse, and
focus events) all you have to do is override the provided method. These methods
are defined in the base class
Component, so
they’re available in virtually all the controls that you might place on a
form. However, you should be aware that this approach is deprecated in Java
1.1, so although you might see legacy code using this
technique you should use the Java 1.1 approaches (described later in this
chapter) instead.
Component method |
When it’s
called |
---|---|
action (Event evt, Object
what) |
When the “typical”
event occurs for this component (for example, when a button is pushed or a
drop-down list item is selected) |
keyDown (Event evt, int
key) |
A key is pressed when this
component has the focus. The second argument is the key that was pressed and is
redundantly copied from evt.key. |
keyUp(Event evt, int
key) |
A key is released when this
component has the focus. |
lostFocus(Event evt, Object
what) |
The focus has moved away from the
target. Normally, what is redundantly copied from
evt.arg. |
gotFocus(Event evt, Object
what) |
The focus has moved into the
target. |
mouseDown(Event evt,
|
A mouse down has occurred over the
component, at the coordinates x, y. |
mouseUp(Event evt, int x, int
y) |
A mouse up has occurred over the
component. |
mouseMove(Event evt, int x, int
y) |
The mouse has moved while
it’s over the component. |
mouseDrag(Event evt, int x, int
y) |
The mouse is being dragged after a
mouseDown occurred over the component. All drag events are reported to
the component in which the mouseDown occurred until there is a
mouseUp. |
mouseEnter(Event evt, int x, int
y) |
The mouse wasn’t over the
component before, but now it is. |
mouseExit(Event evt, int x, int
y) |
The mouse used to be over the
component, but now it isn’t. |
You can see that each method
receives an Event object along with some information that you’ll
typically need when you’re handling that particular situation – with
a mouse event, for example, it’s likely that you’ll want to know the
coordinates where the mouse event occurred. It’s interesting to note that
when Component’s handleEvent( ) calls any of these
methods (the typical case), the extra arguments are always redundant as they are
contained within the Event object. In fact, if you look at the source
code for Component.handleEvent( ) you can see that it explicitly
plucks the additional arguments out of the Event object. (This might be
considered inefficient coding in some languages, but remember that Java’s
focus is on safety, not necessarily speed.)
To prove to yourself that these
events are in fact being called and as an interesting experiment, it’s
worth creating an applet that overrides each of the methods above (except for
action( ), which is overridden in many other places in this chapter)
and displays data about each of the events as they happen.
This example also shows you how to
make your own button object because that’s what is used as the target of
all the events of interest. You might first (naturally) assume that to make a
new button, you’d inherit from
Button. But this
doesn’t work. Instead, you inherit from
Canvas (a much more
generic component) and paint your button on that canvas by overriding the
paint( ) method. As
you’ll see, it’s really too bad that overriding Button
doesn’t work, since there’s a bit of code involved to paint the
button. (If you don’t believe me, try exchanging Button for
Canvas in this example, and remember to call the base-class constructor
super(label). You’ll see that the button doesn’t get painted
and the events don’t get handled.)
The myButton class is
specific: it works only with an AutoEvent “parent window”
(not a base class, but the window in which this button is created and lives).
With this knowledge, myButton can reach into the parent window and
manipulate its text fields, which is what’s necessary to be able to write
the status information into the fields of the parent. Of course this is a much
more limited solution, since myButton can be used only in conjunction
with AutoEvent. This kind of code is sometimes called “highly
coupled.” However, to make myButton more generic requires a lot
more effort that isn’t warranted for this example (and possibly for many
of the applets that you will write). Again, keep in mind that the following code
uses APIs that are deprecated in Java
1.1.
//: AutoEvent.java // Alternatives to action() import java.awt.*; import java.applet.*; import java.util.*; class MyButton extends Canvas { AutoEvent parent; Color color; String label; MyButton(AutoEvent parent, Color color, String label) { this.label = label; this.parent = parent; this.color = color; } public void paint(Graphics g) { g.setColor(color); int rnd = 30; g.fillRoundRect(0, 0, size().width, size().height, rnd, rnd); g.setColor(Color.black); g.drawRoundRect(0, 0, size().width, size().height, rnd, rnd); FontMetrics fm = g.getFontMetrics(); int width = fm.stringWidth(label); int height = fm.getHeight(); int ascent = fm.getAscent(); int leading = fm.getLeading(); int horizMargin = (size().width - width)/2; int verMargin = (size().height - height)/2; g.setColor(Color.white); g.drawString(label, horizMargin, verMargin + ascent + leading); } public boolean keyDown(Event evt, int key) { TextField t = (TextField)parent.h.get("keyDown"); t.setText(evt.toString()); return true; } public boolean keyUp(Event evt, int key) { TextField t = (TextField)parent.h.get("keyUp"); t.setText(evt.toString()); return true; } public boolean lostFocus(Event evt, Object w) { TextField t = (TextField)parent.h.get("lostFocus"); t.setText(evt.toString()); return true; } public boolean gotFocus(Event evt, Object w) { TextField t = (TextField)parent.h.get("gotFocus"); t.setText(evt.toString()); return true; } public boolean mouseDown(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseDown"); t.setText(evt.toString()); return true; } public boolean mouseDrag(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseDrag"); t.setText(evt.toString()); return true; } public boolean mouseEnter(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseEnter"); t.setText(evt.toString()); return true; } public boolean mouseExit(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseExit"); t.setText(evt.toString()); return true; } public boolean mouseMove(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseMove"); t.setText(evt.toString()); return true; } public boolean mouseUp(Event evt,int x,int y) { TextField t = (TextField)parent.h.get("mouseUp"); t.setText(evt.toString()); return true; } } public class AutoEvent extends Applet { Hashtable h = new Hashtable(); String[] event = { "keyDown", "keyUp", "lostFocus", "gotFocus", "mouseDown", "mouseUp", "mouseMove", "mouseDrag", "mouseEnter", "mouseExit" }; MyButton b1 = new MyButton(this, Color.blue, "test1"), b2 = new MyButton(this, Color.red, "test2"); public void init() { setLayout(new GridLayout(event.length+1,2)); for(int i = 0; i < event.length; i++) { TextField t = new TextField(); t.setEditable(false); add(new Label(event[i], Label.CENTER)); add(t); h.put(event[i], t); } add(b1); add(b2); } } ///:~
You can see the constructor uses
the technique of using the same name for the argument as what it’s
assigned to, and differentiating between the two using
this:
this.label = label;
The paint( ) method
starts out simple: it fills a “round rectangle” with the
button’s color, and then draws a black line around it. Notice the use of
size( ) to determine the width and height of the component (in
pixels, of course). After this, paint( ) seems quite complicated
because there’s a lot of calculation going on to figure out how to center
the button’s label inside the button using the “font metrics.”
You can get a pretty good idea of what’s going on by looking at the method
call, and it turns out that this is pretty stock code, so you can just cut and
paste it when you want to center a label inside any component.
You can’t understand exactly
how the keyDown( ), keyUp( ), etc. methods work until
you look down at the AutoEvent class. This contains a
Hashtable to hold the strings representing the
type of event and the TextField where information about that event is
held. Of course, these could have been created statically rather than putting
them in a Hashtable, but I think you’ll agree that it’s a lot
easier to use and change. In particular, if you need to add or remove a new type
of event in AutoEvent, you simply add or remove a string in the
event array – everything else happens
automatically.
The place where you look up the
strings is in the keyDown( ), keyUp( ), etc. methods
back in MyButton. Each of these methods uses the parent handle to
reach back to the parent window. Since that parent is an AutoEvent it
contains the Hashtable h, and the get( ) method, when
provided with the appropriate String, will produce a handle to an
Object that we happen to know is a TextField – so it is cast
to that. Then the Event object is converted to its String
representation, which is displayed in the TextField.
It turns out this example is rather
fun to play with since you can really see what’s going on with the events
in your
program.
For safety’s sake, applets
are quite restricted and there are many things you can’t do. You can
generally answer the question of what an applet is able to do by looking at what
it is supposed to do: extend the functionality of a Web page in a
browser. Since, as a net surfer, you never really know if a Web page is from a
friendly place or not, you want any code that it runs to be safe. So the biggest
restrictions you’ll notice are probably:
1) An applet can’t touch
the local disk. This means writing or reading, since you
wouldn’t want an applet to read and transmit important information about
you across the Web. Writing is prevented, of course, since that would be an open
invitation to a virus. These restrictions can be relaxed when digital signing is
fully implemented.
2) An applet can’t have
menus. (Note: this is fixed in Swing) This is probably less oriented toward
safety and more toward reducing confusion. You might have noticed that an applet
looks like it blends right in as part of a Web page; you often don’t see
the boundaries of the applet. There’s no frame or title bar to hang the
menu from, other than the one belonging to the Web browser. Perhaps the design
could be changed to allow you to merge your applet menu with the browser menu
– that would be complicated and would also get a bit too close to the edge
of safety by allowing the applet to affect its environment.
3) Dialog boxes are
“untrusted.” In Java, dialog boxes present a bit of a quandary.
First of all, they’re not exactly disallowed in applets but they’re
heavily discouraged. If you pop up a dialog box from within an applet
you’ll get an “untrusted applet” message attached to that
dialog. This is because, in theory, it would be possible to fool the user into
thinking that they’re dealing with a regular native application and to get
them to type in their credit card number, which then goes across the Web. After
seeing the kinds of GUIs that the AWT produces you might have a hard time
believing anybody could be fooled that way. But an applet is always
attached to a Web page and visible within your Web browser, while a dialog box
is detached so in theory it could be possible. As a result it will be rare to
see an applet that uses a dialog box.
Many applet restrictions are
relaxed for trusted applets (those signed by a trusted source) in newer
browsers.
If you can live within the
restrictions, applets have definite advantages, especially when building
client/server or other networked
applications:
It’s possible to see that for
safety’s sake you can have only limited behavior within an applet. In a
real sense, the applet is a temporary extension to the Web browser so its
functionality must be limited along with its knowledge and control. There are
times, however, when you’d like to make a windowed program do something
else than sit on a Web page, and perhaps you’d like it to do some of the
things a “regular” application can do and yet have the vaunted
instant portability provided by Java. In previous chapters in this book
we’ve made command-line applications, but in some operating environments
(the Macintosh, for example) there isn’t a command line. So for any number
of reasons you’d like to build a windowed, non-applet program using Java.
This is certainly a reasonable desire.
A Java windowed application can
have menus and dialog boxes (impossible or difficult with an applet), and yet if
you’re using an older version of Java you sacrifice the native operating
environment’s look and feel. The JFC/Swing library allows you to make an
application that preserves the look and feel of the underlying operating
environment. If you want to build windowed applications, it makes sense to do so
only if you can use the latest version of Java and associated tools so you can
deliver applications that won’t confound your users. If for some reason
you’re forced to use an older version of Java, think hard before
committing to building a significant windowed
application.
It’s impossible to put a menu
directly on an applet (in Java 1.0 and Java 1.1; the Swing library does
allow it), so they’re for applications. Go ahead, try it if you
don’t believe me and you’re sure that it would make sense to have
menus on applets. There’s no
setMenuBar( ) method
in Applet and that’s the way a menu is attached. (You’ll see
later that it’s possible to spawn a Frame from within an
Applet, and the Frame can contain menus.)
There are four different types of
MenuComponent, all
derived from that abstract class:
MenuBar (you can have one
MenuBar only on a particular
Frame),
Menu to hold one
individual drop-down menu or submenu,
MenuItem to represent one
single element on a menu, and
CheckboxMenuItem, which
is derived from MenuItem and produces a checkmark to indicate whether
that menu item is selected.
Unlike a system that uses
resources, with Java and the AWT you must hand assemble all the menus in source
code. Here are the ice cream flavors again, used to create
menus:
//: Menu1.java // Menus work only with Frames. // Shows submenus, checkbox menu items // and swapping menus. import java.awt.*; public class Menu1 extends Frame { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; TextField t = new TextField("No flavor", 30); MenuBar mb1 = new MenuBar(); Menu f = new Menu("File"); Menu m = new Menu("Flavors"); Menu s = new Menu("Safety"); // Alternative approach: CheckboxMenuItem[] safety = { new CheckboxMenuItem("Guard"), new CheckboxMenuItem("Hide") }; MenuItem[] file = { new MenuItem("Open"), new MenuItem("Exit") }; // A second menu bar to swap to: MenuBar mb2 = new MenuBar(); Menu fooBar = new Menu("fooBar"); MenuItem[] other = { new MenuItem("Foo"), new MenuItem("Bar"), new MenuItem("Baz"), }; Button b = new Button("Swap Menus"); public Menu1() { for(int i = 0; i < flavors.length; i++) { m.add(new MenuItem(flavors[i])); // Add separators at intervals: if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) s.add(safety[i]); f.add(s); for(int i = 0; i < file.length; i++) f.add(file[i]); mb1.add(f); mb1.add(m); setMenuBar(mb1); t.setEditable(false); add("Center", t); // Set up the system for swapping menus: add("North", b); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); mb2.add(fooBar); } public boolean handleEvent(Event evt) { if(evt.id == Event.WINDOW_DESTROY) System.exit(0); else return super.handleEvent(evt); return true; } public boolean action(Event evt, Object arg) { if(evt.target.equals(b)) { MenuBar m = getMenuBar(); if(m == mb1) setMenuBar(mb2); else if (m == mb2) setMenuBar(mb1); } else if(evt.target instanceof MenuItem) { if(arg.equals("Open")) { String s = t.getText(); boolean chosen = false; for(int i = 0; i < flavors.length; i++) if(s.equals(flavors[i])) chosen = true; if(!chosen) t.setText("Choose a flavor first!"); else t.setText("Opening "+ s +". Mmm, mm!"); } else if(evt.target.equals(file[1])) System.exit(0); // CheckboxMenuItems cannot use String // matching; you must match the target: else if(evt.target.equals(safety[0])) t.setText("Guard the Ice Cream! " + "Guarding is " + safety[0].getState()); else if(evt.target.equals(safety[1])) t.setText("Hide the Ice Cream! " + "Is it cold? " + safety[1].getState()); else t.setText(arg.toString()); } else return super.action(evt, arg); return true; } public static void main(String[] args) { Menu1 f = new Menu1(); f.resize(300,200); f.show(); } } ///:~
In this program I avoided the
typical long lists of add( ) calls for each menu because that seemed
like a lot of unnecessary typing. Instead, I placed the menu items into arrays
and then simply stepped through each array calling add( ) in a
for loop. This makes adding or subtracting a menu item less
tedious.
As an alternative approach (which I
find less desirable since it requires more typing), the CheckboxMenuItems
are created in an array of handles called safety; this is true for the
arrays file and other as well.
This program creates not one but
two MenuBars to demonstrate that menu bars can be actively swapped while
the program is running. You can see how a MenuBar is made up of
Menus, and each Menu is made up of MenuItems,
CheckboxMenuItems, or even other Menus (which produce submenus).
When a MenuBar is assembled it can be installed into the current program
with the setMenuBar( ) method. Note that when the button is pressed,
it checks to see which menu is currently installed using
getMenuBar( ), then puts the other menu bar in its
place.
When testing for
“Open,” notice that spelling and capitalization are critical, but
Java signals no error if there is no match with “Open.” This kind of
string comparison is a clear source of programming errors.
The checking and un-checking of the
menu items is taken care of automatically, but dealing with CheckboxMenuItems
can be a bit surprising since for some reason they don’t allow string
matching. (Although string matching isn’t a good approach, this seems
inconsistent.) So you can match only the target object and not its label. As
shown, the getState( )
method can be used to reveal the state. You can also change the state of a
CheckboxMenuItem with setState( ).
You might think that one menu could
reasonably reside on more than one menu bar. This does seem to make sense
because all you’re passing to the MenuBar add( ) method
is a handle. However, if you try this, the behavior will be strange and not what
you expect. (It’s difficult to know if this is a bug or if they intended
it to work this way.)
This example also shows what you
need to do to create an application instead of an applet. (Again, because an
application can support menus and an applet cannot directly have a menu.)
Instead of inheriting from Applet, you inherit from Frame. Instead
of init( ) to set things up, you make a constructor for your class.
Finally, you create a main( ) and in that you build an object of
your new type, resize it, and then call show( ). It’s
different from an applet in only a few small places, but it’s now a
standalone
windowed application and you’ve got
menus.
A
dialog box is a window that pops
up out of another window. Its purpose is to deal with some specific issue
without cluttering the original window with those details. Dialog boxes are
heavily used in windowed programming environments, but as mentioned previously,
rarely used in applets.
To create a dialog box, you inherit
from Dialog, which is
just another kind of Window, like a Frame. Unlike a Frame,
a Dialog cannot have a menu bar or change the cursor, but other than that
they’re quite similar. A dialog has a layout manager (which defaults to
BorderLayout) and you override action( ) etc., or
handleEvent( ) to deal with events. One significant difference
you’ll want to note in handleEvent( ): when the
WINDOW_DESTROY event
occurs, you don’t want to shut down the application! Instead, you release
the resources used by the dialog’s window by calling
dispose( ).
In the following example, the
dialog box is made up of a grid (using GridLayout) of a special kind of
button that is defined here as class ToeButton. This button draws a frame
around itself and, depending on its state, a blank, an “x,” or an
“o” in the middle. It starts out blank, and then depending on whose
turn it is, changes to an “x” or an “o.” However, it
will also flip back and forth between “x” and “o” when
you click on the button. (This makes the tic-tac-toe concept only slightly more
annoying than it already is.) In addition, the dialog box can be set up for any
number of rows and columns by changing numbers in the main application
window.
//: ToeTest.java // Demonstration of dialog boxes // and creating your own components import java.awt.*; class ToeButton extends Canvas { int state = ToeDialog.BLANK; ToeDialog parent; ToeButton(ToeDialog parent) { this.parent = parent; } public void paint(Graphics g) { int x1 = 0; int y1 = 0; int x2 = size().width - 1; int y2 = size().height - 1; g.drawRect(x1, y1, x2, y2); x1 = x2/4; y1 = y2/4; int wide = x2/2; int high = y2/2; if(state == ToeDialog.XX) { g.drawLine(x1, y1, x1 + wide, y1 + high); g.drawLine(x1, y1 + high, x1 + wide, y1); } if(state == ToeDialog.OO) { g.drawOval(x1, y1, x1+wide/2, y1+high/2); } } public boolean mouseDown(Event evt, int x, int y) { if(state == ToeDialog.BLANK) { state = parent.turn; parent.turn= (parent.turn == ToeDialog.XX ? ToeDialog.OO : ToeDialog.XX); } else state = (state == ToeDialog.XX ? ToeDialog.OO : ToeDialog.XX); repaint(); return true; } } class ToeDialog extends Dialog { // w = number of cells wide // h = number of cells high static final int BLANK = 0; static final int XX = 1; static final int OO = 2; int turn = XX; // Start with x's turn public ToeDialog(Frame parent, int w, int h) { super(parent, "The game itself", false); setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) add(new ToeButton(this)); resize(w * 50, h * 50); } public boolean handleEvent(Event evt) { if(evt.id == Event.WINDOW_DESTROY) dispose(); else return super.handleEvent(evt); return true; } } public class ToeTest extends Frame { TextField rows = new TextField("3"); TextField cols = new TextField("3"); public ToeTest() { setTitle("Toe Test"); Panel p = new Panel(); p.setLayout(new GridLayout(2,2)); p.add(new Label("Rows", Label.CENTER)); p.add(rows); p.add(new Label("Columns", Label.CENTER)); p.add(cols); add("North", p); add("South", new Button("go")); } public boolean handleEvent(Event evt) { if(evt.id == Event.WINDOW_DESTROY) System.exit(0); else return super.handleEvent(evt); return true; } public boolean action(Event evt, Object arg) { if(arg.equals("go")) { Dialog d = new ToeDialog( this, Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.show(); } else return super.action(evt, arg); return true; } public static void main(String[] args) { Frame f = new ToeTest(); f.resize(200,100); f.show(); } } ///:~
The ToeButton class keeps a
handle to its parent, which must be of type ToeDialog. As before, this
introduces high coupling because a ToeButton can be used only with a
ToeDialog, but it solves a number of problems, and in truth it
doesn’t seem like such a bad solution because there’s no other kind
of dialog that’s keeping track of whose turn it is. Of course, you can
take another approach, which is to make ToeDialog.turn a static
member of ToeButton. This eliminates the coupling, but prevents you from
having more than one ToeDialog at a time. (More than one that works
properly, anyway.)
The
paint( ) method is
concerned with the graphics:
drawing the square around the button and drawing the “x” or the
“o.” This is full of tedious calculations, but it’s
straightforward.
A mouse click is captured by the
overridden
mouseDown( ) method,
which first checks to see if the button has anything written on it. If not, the
parent window is queried to find out whose turn it is and that is used to
establish the state of the button. Note that the button then reaches back into
the parent and changes the turn. If the button is already displaying an
“x” or an “o” then that is flopped. You can see in these
calculations the convenient use of the ternary if-else described in Chapter 3.
After a button state change, the button is repainted.
The constructor for
ToeDialog is quite simple: it adds into a GridLayout as many
buttons as you request, then resizes it for 50 pixels on a side for each button.
(If you don’t resize a Window, it won’t show up!) Note that
handleEvent( ) just calls dispose( ) for a
WINDOW_DESTROY so the whole application doesn’t go
away.
ToeTest sets up the whole
application by creating the TextFields (for inputting the rows and
columns of the button grid) and the “go” button. You’ll see in
action( ) that this program uses the less-desirable “string
match” technique for detecting the button press (make sure you get
spelling and capitalization right!). When the button is pressed, the data in the
TextFields must be fetched, and, since they are in String form,
turned into ints using the static
Integer.parseInt( )
method. Once the Dialog is created, the show( ) method must
be called to display and activate it.
You’ll notice that the
ToeDialog object is assigned to a Dialog handle d. This is
an example of upcasting, although it really doesn’t make much difference
here since all that’s happening is the show( ) method is
called. However, if you wanted to call some method that existed only in
ToeDialog you would want to assign to a ToeDialog handle and not
lose the information in an upcast.
Some operating systems have a
number of special built-in dialog boxes to handle the selection of things such
as fonts, colors, printers, and the like. Virtually all graphical operating
systems support the opening and saving of files, however, and so Java’s
FileDialog encapsulates
these for easy use. This, of course, makes no sense at all to use from an applet
since an applet can neither read nor write files on the local disk. (This will
change for trusted applets in newer browsers.)
The following application exercises
the two forms of file dialogs, one for opening and one for saving. Most of the
code should by now be familiar, and all the interesting activities happen in
action( ) for the two different button clicks:
//: FileDialogTest.java // Demonstration of File dialog boxes import java.awt.*; public class FileDialogTest extends Frame { TextField filename = new TextField(); TextField directory = new TextField(); Button open = new Button("Open"); Button save = new Button("Save"); public FileDialogTest() { setTitle("File Dialog Test"); Panel p = new Panel(); p.setLayout(new FlowLayout()); p.add(open); p.add(save); add("South", p); directory.setEditable(false); filename.setEditable(false); p = new Panel(); p.setLayout(new GridLayout(2,1)); p.add(filename); p.add(directory); add("North", p); } public boolean handleEvent(Event evt) { if(evt.id == Event.WINDOW_DESTROY) System.exit(0); else return super.handleEvent(evt); return true; } public boolean action(Event evt, Object arg) { if(evt.target.equals(open)) { // Two arguments, defaults to open file: FileDialog d = new FileDialog(this, "What file do you want to open?"); d.setFile("*.java"); // Filename filter d.setDirectory("."); // Current directory d.show(); String openFile; if((openFile = d.getFile()) != null) { filename.setText(openFile); directory.setText(d.getDirectory()); } else { filename.setText("You pressed cancel"); directory.setText(""); } } else if(evt.target.equals(save)) { FileDialog d = new FileDialog(this, "What file do you want to save?", FileDialog.SAVE); d.setFile("*.java"); d.setDirectory("."); d.show(); String saveFile; if((saveFile = d.getFile()) != null) { filename.setText(saveFile); directory.setText(d.getDirectory()); } else { filename.setText("You pressed cancel"); directory.setText(""); } } else return super.action(evt, arg); return true; } public static void main(String[] args) { Frame f = new FileDialogTest(); f.resize(250,110); f.show(); } } ///:~
For an “open file”
dialog, you use the constructor that takes two arguments; the first is the
parent window handle and the second is the title for the title bar of the
FileDialog. The method
setFile( ) provides
an initial file name – presumably the native OS supports wildcards, so in
this example all the .java files will initially be displayed. The
setDirectory( )
method chooses the directory where the file selection will begin. (In general,
the OS allows the user to change directories.)
The
show( ) command
doesn’t return until the dialog is closed. The FileDialog object
still exists, so you can read data from it. If you call
getFile( ) and it
returns null it means the user canceled out of the dialog. Both the file
name and the results of
getDirectory( ) are
displayed in the TextFields.
The button for saving works the
same way, except that it uses a different constructor for the FileDialog.
This constructor takes three arguments and the third argument must be either
FileDialog.SAVE or
FileDialog.OPEN.
In Java 1.1
a dramatic change has been accomplished in the creation of the new AWT. Most of
this change revolves around the
new event model used in Java
1.1: as bad, awkward, and non-object-oriented as the old event model was, the
new event model is possibly the most elegant I have seen. It’s difficult
to understand how such a bad design (the old AWT) and such a good one (the new
event model) could come out of the same group. This new way of thinking about
events seems to drop so easily into your mind that the issue no longer becomes
an impediment; instead, it’s a tool that helps you design the system.
It’s also essential for Java Beans, described later in the
chapter.
Instead of the non-object-oriented
cascaded if statements in the old AWT, the new approach designates
objects as “sources” and “listeners” of events. As you
will see, the use of inner classes is integral to the object-oriented nature of
the new event model. In addition, events are now represented in a class
hierarchy instead of a single class, and you can create your own event
types.
You’ll also find, if
you’ve programmed with the old AWT, that Java 1.1 has made a number of
what might seem like gratuitous name changes. For example,
setSize( ) replaces resize( ). This will make sense when
you learn about Java Beans, because Beans use a particular naming convention.
The names had to be modified to make the standard AWT components into
Beans.
Java 1.1 continues to support the
old AWT to ensure backward compatibility with existing programs. Without fully
admitting disaster, the online documents for Java 1.1 list all the problems
involved with programming the old AWT and describe how those problems are
addressed in the new AWT.
Clipboard operations are supported
in 1.1, although drag-and-drop “will be supported in a future
release.” You can access the desktop color scheme so your Java program can
fit in with the rest of the desktop. Pop-up menus are available, and there are
some improvements for graphics and images. Mouseless operation is supported.
There is a simple API for printing and simplified support for
scrolling.
In the new event model a component
can initiate (“fire”) an event. Each type of event is represented by
a distinct class. When an event is fired, it is received by one or more
“listeners,” which act on that event. Thus, the source of an event
and the place where the event is handled can be separate.
Each
event listener is an object of a
class that implements a particular type of listener interface. So as a
programmer, all you do is create a listener object and register it with the
component that’s firing the event. This registration is performed by
calling a addXXXListener( ) method in the event-firing component, in
which XXX represents the type of event listened for. You can easily know
what types of events can be handled by noticing the names of the
addListener methods, and if you
try to listen for the wrong events you’ll find out your mistake at compile
time. Java Beans also uses the names of the addListener methods to determine
what a Bean can do.
All of your event logic, then, will
go inside a listener class. When you create a listener class, the sole
restriction is that it must implement the appropriate interface. You can create
a global listener class, but this is a situation in which
inner
classes tend to be quite useful, not only because they provide a logical
grouping of your listener classes inside the UI or business logic classes they
are serving, but because (as you shall see later) the fact that an inner class
object keeps a handle to its parent object provides a nice way to call across
class and subsystem boundaries.
A simple example will make this
clear. Consider the Button2.java example from earlier in this chapter.
//: Button2New.java // Capturing button presses import java.awt.*; import java.awt.event.*; // Must add this import java.applet.*; public class Button2New extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public void init() { b1.addActionListener(new B1()); b2.addActionListener(new B2()); add(b1); add(b2); } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { getAppletContext().showStatus("Button 1"); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { getAppletContext().showStatus("Button 2"); } } /* The old way: public boolean action(Event evt, Object arg) { if(evt.target.equals(b1)) getAppletContext().showStatus("Button 1"); else if(evt.target.equals(b2)) getAppletContext().showStatus("Button 2"); // Let the base class handle it: else return super.action(evt, arg); return true; // We've handled it here } */ } ///:~
So you can compare the two
approaches, the old code is left in as a comment. In init( ), the
only change is the addition of the two lines:
b1.addActionListener(new B1()); b2.addActionListener(new B2());
addActionListener( )
tells a button which object to activate when the button is pressed. The classes
B1 and B2 are inner classes that implement the interface
ActionListener. This interface contains a single method
actionPerformed( ) (meaning “This is the action that will be
performed when the event is fired”). Note that
actionPerformed( ) does not take a generic event, but rather a
specific type of event, ActionEvent. So you don’t need to bother
testing and downcasting the argument if you want to extract specific
ActionEvent information.
One of the nicest things about
actionPerformed( ) is how simple it is. It’s just a method
that gets called. Compare it to the old action( ) method, in which
you must figure out what happened and act appropriately, and also worry about
calling the base class version of action( ) and return a value to
indicate whether it’s been handled. With the new event model you know that
all the event-detection logic is taken care of so you don’t have to figure
that out; you just say what happens and you’re done. If you don’t
already prefer this approach over the old one, you will
soon.
All the AWT components have been
changed to include addXXXListener( ) and
removeXXXListener( ) methods so that the appropriate types of
listeners can be added and removed from each component. You’ll notice that
the “XXX” in each case also represents the argument for the
method, for example, addFooListener(FooListener fl). The following table
includes the associated events, listeners, methods, and the components that
support those particular events by providing the addXXXListener( )
and removeXXXListener( ) methods.
Event, listener interface and add-
and remove-methods |
Components supporting this
event |
---|---|
ActionEvent |
Button, List,
TextField, MenuItem, and its derivatives including CheckboxMenuItem,
Menu, and PopupMenu |
AdjustmentEvent |
Scrollbar |
ComponentEvent |
Component and its
derivatives, including Button, Canvas, Checkbox,
Choice, Container, Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, Frame, Label,
List, Scrollbar, TextArea, and
TextField |
ContainerEvent |
Container and its
derivatives, including Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, and
Frame |
FocusEvent |
Component and its
derivatives, including Button, Canvas, Checkbox,
Choice, Container, Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, Frame Label,
List, Scrollbar, TextArea, and
TextField |
KeyEvent |
Component and its
derivatives, including Button, Canvas, Checkbox,
Choice, Container, Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, Frame, Label,
List, Scrollbar, TextArea, and
TextField |
MouseEvent (for both clicks
and
motion) |
Component and its
derivatives, including Button, Canvas, Checkbox,
Choice, Container, Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, Frame, Label,
List, Scrollbar, TextArea, and
TextField |
MouseEvent[55]
(for both clicks and
motion) |
Component and its
derivatives, including Button, Canvas, Checkbox,
Choice, Container, Panel, Applet, ScrollPane,
Window, Dialog, FileDialog, Frame, Label,
List, Scrollbar, TextArea, and
TextField |
WindowEvent |
Window and its derivatives,
including Dialog, FileDialog, and Frame |
ItemEvent |
Checkbox,
CheckboxMenuItem, Choice, List, and anything that
implements the ItemSelectable interface |
TextEvent |
Anything derived from
TextComponent, including TextArea and
TextField |
You can see that each type of
component supports only certain types of events. It’s helpful to see the
events supported by each component, as shown in the following table:
Component type |
Events supported by this
component |
---|---|
Adjustable |
AdjustmentEvent |
Applet |
ContainerEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
Button |
ActionEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
Canvas |
FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
Checkbox |
ItemEvent, FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
CheckboxMenuItem |
ActionEvent,
ItemEvent |
Choice |
ItemEvent, FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
Component |
FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
Container |
ContainerEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
Dialog |
ContainerEvent, WindowEvent,
FocusEvent, KeyEvent, MouseEvent, ComponentEvent |
FileDialog |
ContainerEvent, WindowEvent,
FocusEvent, KeyEvent, MouseEvent, ComponentEvent |
Frame |
ContainerEvent, WindowEvent,
FocusEvent, KeyEvent, MouseEvent, ComponentEvent |
Label |
FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
List |
ActionEvent, FocusEvent,
KeyEvent, MouseEvent, ItemEvent, ComponentEvent |
Menu |
ActionEvent |
MenuItem |
ActionEvent |
Panel |
ContainerEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
PopupMenu |
ActionEvent |
Scrollbar |
AdjustmentEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
ScrollPane |
ContainerEvent, FocusEvent,
KeyEvent, MouseEvent, ComponentEvent |
TextArea |
TextEvent, FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
TextComponent |
TextEvent, FocusEvent, KeyEvent,
MouseEvent, ComponentEvent |
TextField |
ActionEvent, TextEvent,
FocusEvent, KeyEvent, MouseEvent, ComponentEvent |
Window |
ContainerEvent, WindowEvent,
FocusEvent, KeyEvent, MouseEvent, ComponentEvent |
Once you know which events a
particular component supports, you don’t need to look anything up to react
to that event. You simply:
Listener
interface |
Methods in
interface |
---|---|
ActionListener |
actionPerformed(ActionEvent) |
AdjustmentListener |
adjustmentValueChanged( |
ComponentListener |
componentHidden(ComponentEvent) |
ContainerListener |
componentAdded(ContainerEvent) |
FocusListener |
focusGained(FocusEvent) |
KeyListener |
keyPressed(KeyEvent) |
MouseListener |
mouseClicked(MouseEvent) |
MouseMotionListener |
mouseDragged(MouseEvent) |
WindowListener |
windowOpened(WindowEvent) |
ItemListener |
itemStateChanged(ItemEvent) |
TextListener |
textValueChanged(TextEvent) |
In the table above, you can see
that some listener interfaces have only one method. These are trivial to
implement since you’ll implement them only when you want to write that
particular method. However, the listener interfaces that have multiple methods
could be less pleasant to use. For example, something you must always do when
creating an application is provide a WindowListener to the Frame
so that when you get the windowClosing( ) event you can call
System.exit(0) to exit the application. But since WindowListener
is an interface, you must implement all of the other methods even if they
don’t do anything. This can be annoying.
To solve the problem, each of the
listener interfaces that have more than one method are provided with
adapters, the names of which you can see in the table above. Each adapter
provides default methods for each of the interface methods. (Alas,
WindowAdapter does not have a default windowClosing( )
that calls System.exit(0).) Then all you need to do is inherit from the
adapter and override only the methods you need to change. For example, the
typical WindowListener you’ll use looks like this:
class MyWindowListener extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } }
The whole point of the adapters is
to make the creation of listener classes easy.
There is a downside to adapters,
however, in the form of a pitfall. Suppose you write a WindowAdapter like
the one above:
class MyWindowListener extends WindowAdapter { public void WindowClosing(WindowEvent e) { System.exit(0); } }
This doesn’t work, but it
will drive you crazy trying to figure out why, since everything will compile and
run fine – except that closing the window won’t exit the program.
Can you see the problem? It’s in the name of the method:
WindowClosing( ) instead of windowClosing( ). A simple
slip in capitalization results in the addition of a completely new method.
However, this is not the method that’s called when the window is closing,
so you don’t get the desired
results.
Often you’ll want to be able
to create a class that can be invoked as either a window or an applet. To
accomplish this, you simply add a main( ) to your applet that builds
an instance of the applet inside a Frame. As a simple example,
let’s look at Button2New.java modified to work as both an
application and an applet:
//: Button2NewB.java // An application and an applet import java.awt.*; import java.awt.event.*; // Must add this import java.applet.*; public class Button2NewB extends Applet { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); TextField t = new TextField(20); public void init() { b1.addActionListener(new B1()); b2.addActionListener(new B2()); add(b1); add(b2); add(t); } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Button 1"); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Button 2"); } } // To close the application: static class WL extends WindowAdapter { public void windowClosing(WindowEvent e) { System.exit(0); } } // A main() for the application: public static void main(String[] args) { Button2NewB applet = new Button2NewB(); Frame aFrame = new Frame("Button2NewB"); aFrame.addWindowListener(new WL()); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
The inner class WL and the
main( ) are the only two elements added to the applet, and the rest
of the applet is untouched. In fact, you can usually copy and paste the
WL class and main( ) into your own applets with little
modification. The WL class is static so it can be easily created
in main( ). (Remember that an inner class normally needs an outer
class handle when it’s created. Making it static eliminates this
need.) You can see that in main( ), the applet is explicitly
initialized and started since in this case the browser isn’t available to
do it for you. Of course, this doesn’t provide the full behavior of the
browser, which also calls stop( ) and destroy( ), but
for most situations it’s acceptable. If it’s a problem, you
can:
Notice
the last line:
aFrame.setVisible(true);
This is one of the changes in the
Java 1.1 AWT. The show( ) method is
deprecated and setVisible(true) replaces it. These sorts of seemingly
capricious changes will make more sense when you learn about Java Beans later in
the chapter.
This example is also modified to
use a TextField rather than printing to the console or to the browser
status line. One restriction in making a program that’s both an applet and
an application is that you must choose input and output forms that work for both
situations.
There’s another small new
feature of the Java 1.1 AWT shown here. You no longer
need to use the error-prone approach of specifying
BorderLayout positions
using a String. When adding an element to a BorderLayout in Java
1.1, you can say:
aFrame.add(applet, BorderLayout.CENTER);
You name the location with one of
the BorderLayout constants, which can then be checked at compile-time
(rather than just quietly doing the wrong thing, as with the old form). This is
a definite improvement, and will be used throughout the rest of the
book.
Any of the listener classes could
be implemented as anonymous
classes, but there’s always a chance that you might want to use their
functionality elsewhere. However, the window listener is used here only to close
the application’s window so you can safely make it an anonymous class.
Then, in main( ), the line:
aFrame.addWindowListener(new WL());
will become:
aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } });
This has the advantage that it
doesn’t require yet another class name. You must decide for yourself
whether it makes the code easier to understand or more difficult. However, for
the remainder of the book an anonymous inner class will usually be used for the
window listener.
An important
JAR use
is to optimize applet loading. In Java 1.0, people tended to try to cram all
their code into a single Applet class so the client would need only a
single server hit to download the applet code. Not only did this result in
messy, hard to read (and maintain) programs, but the .class file was
still uncompressed so downloading wasn’t as fast as it could have
been.
JAR files change all of that by
compressing all of your .class files into a single file that is
downloaded by the browser. Now you don’t need to create an ugly design to
minimize the number of classes you create, and the user will get a much faster
download time.
Consider the example above. It
looks like Button2NewB is a single class, but in fact it contains three
inner classes, so that’s four in all. Once you’ve compiled the
program, you package it into a JAR file with the line:
jar cf Button2NewB.jar *.class
This assumes that the only
.class files in the current directory are the ones from
Button2NewB.java (otherwise you’ll get extra
baggage).
Now you can create an HTML page
with the new
archive
tag to indicate the name of the JAR file, like this:
<head><title>Button2NewB Example Applet </title></head> <body> <applet code="Button2NewB.class" archive="Button2NewB.jar" width=200 height=150> </applet> </body>
To see a number of examples using
the new event model and to study the way a program can be converted from the old
to the new event model, the following examples revisit many of the issues
demonstrated in the first part of this chapter using the old event model. In
addition, each program is now both an applet and an application so you can run
it with or without a browser.
This is similar to
TextField1.java, but it adds significant extra behavior:
//: TextNew.java // Text fields with Java 1.1 events import java.awt.*; import java.awt.event.*; import java.applet.*; public class TextNew extends Applet { Button b1 = new Button("Get Text"), b2 = new Button("Set Text"); TextField t1 = new TextField(30), t2 = new TextField(30), t3 = new TextField(30); String s = new String(); public void init() { b1.addActionListener(new B1()); b2.addActionListener(new B2()); t1.addTextListener(new T1()); t1.addActionListener(new T1A()); t1.addKeyListener(new T1K()); add(b1); add(b2); add(t1); add(t2); add(t3); } class T1 implements TextListener { public void textValueChanged(TextEvent e) { t2.setText(t1.getText()); } } class T1A implements ActionListener { private int count = 0; public void actionPerformed(ActionEvent e) { t3.setText("t1 Action Event " + count++); } } class T1K extends KeyAdapter { public void keyTyped(KeyEvent e) { String ts = t1.getText(); if(e.getKeyChar() == KeyEvent.VK_BACK_SPACE) { // Ensure it's not empty: if( ts.length() > 0) { ts = ts.substring(0, ts.length() - 1); t1.setText(ts); } } else t1.setText( t1.getText() + Character.toUpperCase( e.getKeyChar())); t1.setCaretPosition( t1.getText().length()); // Stop regular character from appearing: e.consume(); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { s = t1.getSelectedText(); if(s.length() == 0) s = t1.getText(); t1.setEditable(true); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { t1.setText("Inserted by Button 2: " + s); t1.setEditable(false); } } public static void main(String[] args) { TextNew applet = new TextNew(); Frame aFrame = new Frame("TextNew"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
The
TextField t3 is included
as a place to report when the action listener for the TextField t1
is fired. You’ll see that the action listener for a TextField is
fired only when you press the “enter” key.
The TextField t1 has several
listeners attached to it. The T1 listener copies all text from t1
into t2 and the T1K listener forces all characters to upper case.
You’ll notice that the two work together, and if you add the T1K
listener after you add the T1 listener, it doesn’t matter:
all characters will still be forced to upper case in both text fields. It would
seem that keyboard events are always fired before
TextComponent events, and
if you want the characters in t2 to retain the original case that was
typed in, you must do some extra work.
T1K has some other
activities of interest. You must detect a backspace (since you’re
controlling everything now) and perform the deletion. The caret must be
explicitly set to the end of the field; otherwise it won’t behave as you
expect. Finally, to prevent the original character from being handled by the
default mechanism, the event must be “consumed” using the
consume( ) method
that exists for event objects. This tells the system to stop firing the rest of
the event handlers for this particular event.
This example also quietly
demonstrates one of the benefits of the design of inner classes. Note that in
the inner
class:
class T1 implements TextListener { public void textValueChanged(TextEvent e) { t2.setText(t1.getText()); } }
t1 and t2 are
not members of T1, and yet they’re accessible without any
special qualification. This is because an object of an inner class automatically
captures a handle to the outer object that created it, so you can treat members
and methods of the enclosing class object as if they’re yours. As you can
see, this is quite
convenient.[56]
The most significant change to text
areas in Java 1.1 concerns scroll bars. With the
TextArea constructor, you
can now control whether a TextArea will have scroll bars: vertical,
horizontal, both, or neither. This example modifies the earlier Java
1.0 TextArea1.java to show the Java 1.1 scrollbar
constructors:
//: TextAreaNew.java // Controlling scrollbars with the TextArea // component in Java 1.1 import java.awt.*; import java.awt.event.*; import java.applet.*; public class TextAreaNew extends Applet { Button b1 = new Button("Text Area 1"); Button b2 = new Button("Text Area 2"); Button b3 = new Button("Replace Text"); Button b4 = new Button("Insert Text"); TextArea t1 = new TextArea("t1", 1, 30); TextArea t2 = new TextArea("t2", 4, 30); TextArea t3 = new TextArea("t3", 1, 30, TextArea.SCROLLBARS_NONE); TextArea t4 = new TextArea("t4", 10, 10, TextArea.SCROLLBARS_VERTICAL_ONLY); TextArea t5 = new TextArea("t5", 4, 30, TextArea.SCROLLBARS_HORIZONTAL_ONLY); TextArea t6 = new TextArea("t6", 10, 10, TextArea.SCROLLBARS_BOTH); public void init() { b1.addActionListener(new B1L()); add(b1); add(t1); b2.addActionListener(new B2L()); add(b2); add(t2); b3.addActionListener(new B3L()); add(b3); b4.addActionListener(new B4L()); add(b4); add(t3); add(t4); add(t5); add(t6); } class B1L implements ActionListener { public void actionPerformed(ActionEvent e) { t5.append(t1.getText() + "\n"); } } class B2L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.setText("Inserted by Button 2"); t2.append(": " + t1.getText()); t5.append(t2.getText() + "\n"); } } class B3L implements ActionListener { public void actionPerformed(ActionEvent e) { String s = " Replacement "; t2.replaceRange(s, 3, 3 + s.length()); } } class B4L implements ActionListener { public void actionPerformed(ActionEvent e) { t2.insert(" Inserted ", 10); } } public static void main(String[] args) { TextAreaNew applet = new TextAreaNew(); Frame aFrame = new Frame("TextAreaNew"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,725); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
You’ll notice that you can
control the scrollbars only at the time of construction of the TextArea.
Also, even if a TextArea doesn’t have a scrollbar, you can move the
cursor such that scrolling will be forced. (You can see this behavior by playing
with the example.)
As noted previously, check boxes
and radio buttons are both created with the same class,
Checkbox, but radio
buttons are Checkboxes placed into a CheckboxGroup. In either
case, the interesting event is
ItemEvent, for which you
create an
ItemListener.
When dealing with a group of check
boxes or radio buttons, you have a choice. You can either create a new inner
class to handle the event for each different Checkbox or you can create
one inner class that determines which Checkbox was clicked and register a
single object of that inner class with each Checkbox object. The
following example shows both approaches:
//: RadioCheckNew.java // Radio buttons and Check Boxes in Java 1.1 import java.awt.*; import java.awt.event.*; import java.applet.*; public class RadioCheckNew extends Applet { TextField t = new TextField(30); Checkbox[] cb = { new Checkbox("Check Box 1"), new Checkbox("Check Box 2"), new Checkbox("Check Box 3") }; CheckboxGroup g = new CheckboxGroup(); Checkbox cb4 = new Checkbox("four", g, false), cb5 = new Checkbox("five", g, true), cb6 = new Checkbox("six", g, false); public void init() { t.setEditable(false); add(t); ILCheck il = new ILCheck(); for(int i = 0; i < cb.length; i++) { cb[i].addItemListener(il); add(cb[i]); } cb4.addItemListener(new IL4()); cb5.addItemListener(new IL5()); cb6.addItemListener(new IL6()); add(cb4); add(cb5); add(cb6); } // Checking the source: class ILCheck implements ItemListener { public void itemStateChanged(ItemEvent e) { for(int i = 0; i < cb.length; i++) { if(e.getSource().equals(cb[i])) { t.setText("Check box " + (i + 1)); return; } } } } // vs. an individual class for each item: class IL4 implements ItemListener { public void itemStateChanged(ItemEvent e) { t.setText("Radio button four"); } } class IL5 implements ItemListener { public void itemStateChanged(ItemEvent e) { t.setText("Radio button five"); } } class IL6 implements ItemListener { public void itemStateChanged(ItemEvent e) { t.setText("Radio button six"); } } public static void main(String[] args) { RadioCheckNew applet = new RadioCheckNew(); Frame aFrame = new Frame("RadioCheckNew"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
ILCheck has the advantage
that it automatically adapts when you add or subtract Checkboxes. Of
course, you can use this with radio buttons as well. It should be used, however,
only when your logic is general enough to support this approach. Otherwise
you’ll end up with a cascaded if statement, a sure sign that you
should revert to using independent listener classes.
Drop-down lists
(Choice) in Java
1.1 also use ItemListeners to notify you when a
choice has changed:
//: ChoiceNew.java // Drop-down lists with Java 1.1 import java.awt.*; import java.awt.event.*; import java.applet.*; public class ChoiceNew extends Applet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; TextField t = new TextField(100); Choice c = new Choice(); Button b = new Button("Add items"); int count = 0; public void init() { t.setEditable(false); for(int i = 0; i < 4; i++) c.addItem(description[count++]); add(t); add(c); add(b); c.addItemListener(new CL()); b.addActionListener(new BL()); } class CL implements ItemListener { public void itemStateChanged(ItemEvent e) { t.setText("index: " + c.getSelectedIndex() + " " + e.toString()); } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { if(count < description.length) c.addItem(description[count++]); } } public static void main(String[] args) { ChoiceNew applet = new ChoiceNew(); Frame aFrame = new Frame("ChoiceNew"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(750,100); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
Nothing else here is particularly
new (except that Java 1.1 has significantly fewer bugs
in the UI classes).
You’ll recall that one of the
problems with the Java 1.0
List design is that it
took extra work to make it do what you’d expect: react to a single click
on one of the list elements. Java 1.1 has solved this
problem:
//: ListNew.java // Java 1.1 Lists are easier to use import java.awt.*; import java.awt.event.*; import java.applet.*; public class ListNew extends Applet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; // Show 6 items, allow multiple selection: List lst = new List(6, true); TextArea t = new TextArea(flavors.length, 30); Button b = new Button("test"); int count = 0; public void init() { t.setEditable(false); for(int i = 0; i < 4; i++) lst.addItem(flavors[count++]); add(t); add(lst); add(b); lst.addItemListener(new LL()); b.addActionListener(new BL()); } class LL implements ItemListener { public void itemStateChanged(ItemEvent e) { t.setText(""); String[] items = lst.getSelectedItems(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { if(count < flavors.length) lst.addItem(flavors[count++], 0); } } public static void main(String[] args) { ListNew applet = new ListNew(); Frame aFrame = new Frame("ListNew"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(300,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
You can see that no extra logic is
required to support a single click on a list item. You just attach a listener
like you do everywhere else.
The event handling for menus does
seem to benefit from the Java 1.1 event model, but
Java’s approach to menus is still messy and requires a lot of hand coding.
The right medium for a menu seems to be a resource rather than a lot of code.
Keep in mind that program-building tools will generally handle the creation of
menus for you, so that will reduce the pain somewhat (as long as they will also
handle the maintenance!).
In addition, you’ll find the
events for menus are inconsistent and can lead to confusion:
MenuItems use
ActionListeners, but
CheckboxMenuItems use
ItemListeners. The
Menu objects can also
support ActionListeners, but that’s not usually helpful. In
general, you’ll attach listeners to each MenuItem or
CheckboxMenuItem, but the following example (revised from the earlier
version) also shows ways to combine the capture of multiple menu components into
a single listener class. As you’ll see, it’s probably not worth the
hassle to do this.
//: MenuNew.java // Menus in Java 1.1 import java.awt.*; import java.awt.event.*; public class MenuNew extends Frame { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; TextField t = new TextField("No flavor", 30); MenuBar mb1 = new MenuBar(); Menu f = new Menu("File"); Menu m = new Menu("Flavors"); Menu s = new Menu("Safety"); // Alternative approach: CheckboxMenuItem[] safety = { new CheckboxMenuItem("Guard"), new CheckboxMenuItem("Hide") }; MenuItem[] file = { // No menu shortcut: new MenuItem("Open"), // Adding a menu shortcut is very simple: new MenuItem("Exit", new MenuShortcut(KeyEvent.VK_E)) }; // A second menu bar to swap to: MenuBar mb2 = new MenuBar(); Menu fooBar = new Menu("fooBar"); MenuItem[] other = { new MenuItem("Foo"), new MenuItem("Bar"), new MenuItem("Baz"), }; // Initialization code: { ML ml = new ML(); CMIL cmil = new CMIL(); safety[0].setActionCommand("Guard"); safety[0].addItemListener(cmil); safety[1].setActionCommand("Hide"); safety[1].addItemListener(cmil); file[0].setActionCommand("Open"); file[0].addActionListener(ml); file[1].setActionCommand("Exit"); file[1].addActionListener(ml); other[0].addActionListener(new FooL()); other[1].addActionListener(new BarL()); other[2].addActionListener(new BazL()); } Button b = new Button("Swap Menus"); public MenuNew() { FL fl = new FL(); for(int i = 0; i < flavors.length; i++) { MenuItem mi = new MenuItem(flavors[i]); mi.addActionListener(fl); m.add(mi); // Add separators at intervals: if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) s.add(safety[i]); f.add(s); for(int i = 0; i < file.length; i++) f.add(file[i]); mb1.add(f); mb1.add(m); setMenuBar(mb1); t.setEditable(false); add(t, BorderLayout.CENTER); // Set up the system for swapping menus: b.addActionListener(new BL()); add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); mb2.add(fooBar); } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { MenuBar m = getMenuBar(); if(m == mb1) setMenuBar(mb2); else if (m == mb2) setMenuBar(mb1); } } class ML implements ActionListener { public void actionPerformed(ActionEvent e) { MenuItem target = (MenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Open")) { String s = t.getText(); boolean chosen = false; for(int i = 0; i < flavors.length; i++) if(s.equals(flavors[i])) chosen = true; if(!chosen) t.setText("Choose a flavor first!"); else t.setText("Opening "+ s +". Mmm, mm!"); } else if(actionCommand.equals("Exit")) { dispatchEvent( new WindowEvent(MenuNew.this, WindowEvent.WINDOW_CLOSING)); } } } class FL implements ActionListener { public void actionPerformed(ActionEvent e) { MenuItem target = (MenuItem)e.getSource(); t.setText(target.getLabel()); } } // Alternatively, you can create a different // class for each different MenuItem. Then you // Don't have to figure out which one it is: class FooL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Foo selected"); } } class BarL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Bar selected"); } } class BazL implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText("Baz selected"); } } class CMIL implements ItemListener { public void itemStateChanged(ItemEvent e) { CheckboxMenuItem target = (CheckboxMenuItem)e.getSource(); String actionCommand = target.getActionCommand(); if(actionCommand.equals("Guard")) t.setText("Guard the Ice Cream! " + "Guarding is " + target.getState()); else if(actionCommand.equals("Hide")) t.setText("Hide the Ice Cream! " + "Is it cold? " + target.getState()); } } public static void main(String[] args) { MenuNew f = new MenuNew(); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); f.setSize(300,200); f.setVisible(true); } } ///:~
This code is similar to the
previous (Java 1.0) version, until you get to the
initialization section (marked by the opening brace right after the comment
“Initialization code:”). Here you can see the ItemListeners
and ActionListeners attached to the various menu components.
Java 1.1
supports
“menu
shortcuts,” so you can select a menu item using the keyboard instead of
the mouse. These are quite simple; you just use the overloaded MenuItem
constructor that takes as a second argument a MenuShortcut object. The
constructor for MenuShortcut takes the key of interest, which magically
appears on the menu item when it drops down. The example above adds Control-E to
the “Exit” menu item.
You can also see the use of
setActionCommand( ).
This seems a bit strange because in each case the “action command”
is exactly the same as the label on the menu component. Why not just use the
label instead of this alternative string? The problem is internationalization.
If you retarget this program to another language, you want to change only the
label in the menu, and not go through the code changing all the logic that will
no doubt introduce new errors. So to make this easy for code that checks the
text string associated with a menu component, the “action command”
can be immutable while the menu label can change. All the code works with the
“action command,” so it’s unaffected by changes to the menu
labels. Note that in this program, not all the menu components are examined for
their action commands, so those that aren’t don’t have their action
command set.
Much of the constructor is the same
as before, with the exception of a couple of calls to add listeners. The bulk of
the work happens in the listeners. In BL, the
MenuBar swapping happens
as in the previous example. In ML, the “figure out who rang”
approach is taken by getting the source of the
ActionEvent and casting
it to a MenuItem, then
getting the action command string to pass it through a cascaded if
statement. Much of this is the same as before, but notice that if
“Exit” is chosen, a new
WindowEvent is created,
passing in the handle of the enclosing class object (MenuNew.this) and
creating a WINDOW_CLOSING
event. This is handed to the
dispatchEvent( )
method of the enclosing class object, which then ends up calling
windowClosing( ) inside the window listener for the Frame
(this listener is created as an anonymous inner class, inside
main( )), just as if the message had been generated the
“normal” way. Through this mechanism, you can
dispatch any message you want in
any circumstances, so it’s quite powerful.
The FL listener is simple
even though it’s handling all the different flavors in the flavor menu.
This approach is useful if you have enough simplicity in your logic, but in
general, you’ll want to take the approach used with FooL,
BarL, and BazL, in which they are each attached to only a single
menu component so no extra detection logic is necessary and you know exactly who
called the listener. Even with the profusion of classes generated this way, the
code inside tends to be smaller and the process is more
foolproof.
This is a direct rewrite of the
earlier ToeTest.java. In this version, however, everything is placed
inside an inner class. Although this completely eliminates the need to keep
track of the object that spawned any class, as was the case in
ToeTest.java, it could be taking the concept of inner classes a bit too
far. At one point, the inner classes are nested four deep! This is the kind of
design in which you need to decide whether the benefit of inner classes is worth
the increased complexity. In addition, when you create a non-static inner
class you’re tying that class to its surrounding class. Sometimes a
standalone class can more easily be reused.
//: ToeTestNew.java // Demonstration of dialog boxes // and creating your own components import java.awt.*; import java.awt.event.*; public class ToeTestNew extends Frame { TextField rows = new TextField("3"); TextField cols = new TextField("3"); public ToeTestNew() { setTitle("Toe Test"); Panel p = new Panel(); p.setLayout(new GridLayout(2,2)); p.add(new Label("Rows", Label.CENTER)); p.add(rows); p.add(new Label("Columns", Label.CENTER)); p.add(cols); add(p, BorderLayout.NORTH); Button b = new Button("go"); b.addActionListener(new BL()); add(b, BorderLayout.SOUTH); } static final int BLANK = 0; static final int XX = 1; static final int OO = 2; class ToeDialog extends Dialog { // w = number of cells wide // h = number of cells high int turn = XX; // Start with x's turn public ToeDialog(int w, int h) { super(ToeTestNew.this, "The game itself", false); setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) add(new ToeButton()); setSize(w * 50, h * 50); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ dispose(); } }); } class ToeButton extends Canvas { int state = BLANK; ToeButton() { addMouseListener(new ML()); } public void paint(Graphics g) { int x1 = 0; int y1 = 0; int x2 = getSize().width - 1; int y2 = getSize().height - 1; g.drawRect(x1, y1, x2, y2); x1 = x2/4; y1 = y2/4; int wide = x2/2; int high = y2/2; if(state == XX) { g.drawLine(x1, y1, x1 + wide, y1 + high); g.drawLine(x1, y1 + high, x1 + wide, y1); } if(state == OO) { g.drawOval(x1, y1, x1 + wide/2, y1 + high/2); } } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { if(state == BLANK) { state = turn; turn = (turn == XX ? OO : XX); } else state = (state == XX ? OO : XX); repaint(); } } } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { Dialog d = new ToeDialog( Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.show(); } } public static void main(String[] args) { Frame f = new ToeTestNew(); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); f.setSize(200,100); f.setVisible(true); } } ///:~
Because statics can be at
only the outer level of the class, inner classes cannot have static data
or static inner classes.
//: FileDialogNew.java // Demonstration of File dialog boxes import java.awt.*; import java.awt.event.*; public class FileDialogNew extends Frame { TextField filename = new TextField(); TextField directory = new TextField(); Button open = new Button("Open"); Button save = new Button("Save"); public FileDialogNew() { setTitle("File Dialog Test"); Panel p = new Panel(); p.setLayout(new FlowLayout()); open.addActionListener(new OpenL()); p.add(open); save.addActionListener(new SaveL()); p.add(save); add(p, BorderLayout.SOUTH); directory.setEditable(false); filename.setEditable(false); p = new Panel(); p.setLayout(new GridLayout(2,1)); p.add(filename); p.add(directory); add(p, BorderLayout.NORTH); } class OpenL implements ActionListener { public void actionPerformed(ActionEvent e) { // Two arguments, defaults to open file: FileDialog d = new FileDialog( FileDialogNew.this, "What file do you want to open?"); d.setFile("*.java"); d.setDirectory("."); // Current directory d.show(); String yourFile = "*.*"; if((yourFile = d.getFile()) != null) { filename.setText(yourFile); directory.setText(d.getDirectory()); } else { filename.setText("You pressed cancel"); directory.setText(""); } } } class SaveL implements ActionListener { public void actionPerformed(ActionEvent e) { FileDialog d = new FileDialog( FileDialogNew.this, "What file do you want to save?", FileDialog.SAVE); d.setFile("*.java"); d.setDirectory("."); d.show(); String saveFile; if((saveFile = d.getFile()) != null) { filename.setText(saveFile); directory.setText(d.getDirectory()); } else { filename.setText("You pressed cancel"); directory.setText(""); } } } public static void main(String[] args) { Frame f = new FileDialogNew(); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); f.setSize(250,110); f.setVisible(true); } } ///:~
It would be nice if all the
conversions were this easy, but they’re usually easy enough, and your code
benefits from the improved
readability.
One of the benefits of the new AWT
event model is flexibility. In the old model you were forced to hard code the
behavior of your program, but with the new model you can add and remove event
behavior with single method calls. The following example demonstrates
this:
//: DynamicEvents.java // The new Java 1.1 event model allows you to // change event behavior dynamically. Also // demonstrates multiple actions for an event. import java.awt.*; import java.awt.event.*; import java.util.*; public class DynamicEvents extends Frame { Vector v = new Vector(); int i = 0; Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public DynamicEvents() { setLayout(new FlowLayout()); b1.addActionListener(new B()); b1.addActionListener(new B1()); b2.addActionListener(new B()); b2.addActionListener(new B2()); add(b1); add(b2); } class B implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("A button was pressed"); } } class CountListener implements ActionListener { int index; public CountListener(int i) { index = i; } public void actionPerformed(ActionEvent e) { System.out.println( "Counted Listener " + index); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Button 1 pressed"); ActionListener a = new CountListener(i++); v.addElement(a); b2.addActionListener(a); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Button 2 pressed"); int end = v.size() -1; if(end >= 0) { b2.removeActionListener( (ActionListener)v.elementAt(end)); v.removeElementAt(end); } } } public static void main(String[] args) { Frame f = new DynamicEvents(); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e){ System.exit(0); } }); f.setSize(300,200); f.show(); } } ///:~
The new twists in this example
are:
This kind of
flexibility provides much greater power in your programming.
You should notice that
event listeners are not guaranteed to be called in the
order they are added (although most implementations do in fact work that
way).
In general you’ll want to
design your classes so that each one does “only one thing.” This is
particularly important when user-interface code is concerned, since it’s
easy to wrap up “what you’re doing” with “how
you’re displaying it.” This kind of coupling prevents code reuse.
It’s much more desirable to separate your “business logic”
from the GUI. This way, you can not only reuse the business logic more easily,
it’s also easier to reuse the GUI.
Another issue is
multi-tiered systems, where the
“business objects”
reside on a completely separate machine. This central location of the business
rules allows changes to be instantly effective for all new transactions, and is
thus a compelling way to set up a system. However, these business objects can be
used in many different applications and so should not be tied to any particular
mode of display. They should just perform the business operations and nothing
more.
The following example shows how
easy it is to separate the business logic from the GUI code:
//: Separation.java // Separating GUI logic and business objects import java.awt.*; import java.awt.event.*; import java.applet.*; class BusinessLogic { private int modifier; BusinessLogic(int mod) { modifier = mod; } public void setModifier(int mod) { modifier = mod; } public int getModifier() { return modifier; } // Some business operations: public int calculation1(int arg) { return arg * modifier; } public int calculation2(int arg) { return arg + modifier; } } public class Separation extends Applet { TextField t = new TextField(20), mod = new TextField(20); BusinessLogic bl = new BusinessLogic(2); Button calc1 = new Button("Calculation 1"), calc2 = new Button("Calculation 2"); public void init() { add(t); calc1.addActionListener(new Calc1L()); calc2.addActionListener(new Calc2L()); add(calc1); add(calc2); mod.addTextListener(new ModL()); add(new Label("Modifier:")); add(mod); } static int getValue(TextField tf) { try { return Integer.parseInt(tf.getText()); } catch(NumberFormatException e) { return 0; } } class Calc1L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation1(getValue(t)))); } } class Calc2L implements ActionListener { public void actionPerformed(ActionEvent e) { t.setText(Integer.toString( bl.calculation2(getValue(t)))); } } class ModL implements TextListener { public void textValueChanged(TextEvent e) { bl.setModifier(getValue(mod)); } } public static void main(String[] args) { Separation applet = new Separation(); Frame aFrame = new Frame("Separation"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(applet, BorderLayout.CENTER); aFrame.setSize(200,200); applet.init(); applet.start(); aFrame.setVisible(true); } } ///:~
You can see that
BusinessLogic is a straightforward class that performs its operations
without even a hint that it might be used in a GUI environment. It just does its
job.
Separation keeps track of
all the UI details, and it talks to BusinessLogic only through its
public interface. All the operations are centered around getting
information back and forth through the UI and the BusinessLogic object.
So Separation, in turn, just does its job. Since Separation knows
only that it’s talking to a BusinessLogic object (that is, it
isn’t highly coupled), it could be massaged into talking to other types of
objects without much trouble.
Thinking in terms of separating UI
from business logic also makes life easier when you’re adapting legacy
code to work with Java.
Inner classes, the new event model,
and the fact that the old event model is still supported along with new library
features that rely on old-style programming has added a new element of
confusion. Now there are even more different ways for people to write unpleasant
code. Unfortunately, this kind of code is showing up in books and article
examples, and even in documentation and examples distributed from Sun! In this
section we’ll look at some misunderstandings about what you should and
shouldn’t do with the new AWT, and end by showing that except in
extenuating circumstances you can always use listener
classes (written as inner
classes) to solve your event-handling needs. Since this is also the simplest and
clearest approach, it should be a relief for you to learn this.
Before looking at anything else,
you should know that although Java 1.1 is
backward-compatible with Java 1.0 (that is, you can
compile and run 1.0 programs with 1.1), you cannot mix the event models within
the same program. That is, you cannot use the old-style
action( ) method in
the same program in which you employ listeners. This can be a problem in a
larger program when you’re trying to integrate old code with a new
program, since you must decide whether to use the old, hard-to-maintain approach
with the new program or to update the old code. This shouldn’t be too much
of a battle since the new approach is so superior to the old.
To give you something to compare
with, here’s an example showing the recommended approach. By now it should
be reasonably familiar and comfortable:
//: GoodIdea.java // The best way to design classes using the new // Java 1.1 event model: use an inner class for // each different event. This maximizes // flexibility and modularity. import java.awt.*; import java.awt.event.*; import java.util.*; public class GoodIdea extends Frame { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public GoodIdea() { setLayout(new FlowLayout()); b1.addActionListener(new B1L()); b2.addActionListener(new B2L()); add(b1); add(b2); } public class B1L implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Button 1 pressed"); } } public class B2L implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Button 2 pressed"); } } public static void main(String[] args) { Frame f = new GoodIdea(); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e){ System.out.println("Window Closing"); System.exit(0); } }); f.setSize(300,200); f.setVisible(true); } } ///:~
This is fairly trivial: each button
has its own listener that prints something out to the console. But notice that
there isn’t an if statement in the entire program, or any statement
that says, “I wonder what caused this event.” Each piece of code is
concerned with doing, not type-checking. This is the best way to write
your code; not only is it easier to conceptualize, but much easier to read and
maintain. Cutting and pasting to create new programs is also much
easier.
The first bad idea is a common and
recommended approach. This makes the main class (typically Applet or
Frame, but it could be any class) implement the various listeners.
Here’s an example:
//: BadIdea1.java // Some literature recommends this approach, // but it's missing the point of the new event // model in Java 1.1 import java.awt.*; import java.awt.event.*; import java.util.*; public class BadIdea1 extends Frame implements ActionListener, WindowListener { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public BadIdea1() { setLayout(new FlowLayout()); addWindowListener(this); b1.addActionListener(this); b2.addActionListener(this); add(b1); add(b2); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if(source == b1) System.out.println("Button 1 pressed"); else if(source == b2) System.out.println("Button 2 pressed"); else System.out.println("Something else"); } public void windowClosing(WindowEvent e) { System.out.println("Window Closing"); System.exit(0); } public void windowClosed(WindowEvent e) {} public void windowDeiconified(WindowEvent e) {} public void windowIconified(WindowEvent e) {} public void windowActivated(WindowEvent e) {} public void windowDeactivated(WindowEvent e) {} public void windowOpened(WindowEvent e) {} public static void main(String[] args) { Frame f = new BadIdea1(); f.setSize(300,200); f.setVisible(true); } } ///:~
The use of this shows up in the
three lines:
addWindowListener(this); b1.addActionListener(this); b2.addActionListener(this);
Since BadIdea1 implements
ActionListener and WindowListener, these lines are certainly
acceptable, and if you’re still stuck in the mode of trying to make fewer
classes to reduce server hits during applet loading, it seems to be a good idea.
However:
The second bad idea is to mix the
two approaches: use inner listener classes, but also implement one or more
listener interfaces as part of the main class. This approach has appeared
without explanation in books and documentation, and I can only assume that the
authors thought they must use the different approaches for different purposes.
But you don’t – in your programming you can probably use inner
listener classes exclusively.
//: BadIdea2.java // An improvement over BadIdea1.java, since it // uses the WindowAdapter as an inner class // instead of implementing all the methods of // WindowListener, but still misses the // valuable modularity of inner classes import java.awt.*; import java.awt.event.*; import java.util.*; public class BadIdea2 extends Frame implements ActionListener { Button b1 = new Button("Button 1"), b2 = new Button("Button 2"); public BadIdea2() { setLayout(new FlowLayout()); addWindowListener(new WL()); b1.addActionListener(this); b2.addActionListener(this); add(b1); add(b2); } public void actionPerformed(ActionEvent e) { Object source = e.getSource(); if(source == b1) System.out.println("Button 1 pressed"); else if(source == b2) System.out.println("Button 2 pressed"); else System.out.println("Something else"); } class WL extends WindowAdapter { public void windowClosing(WindowEvent e) { System.out.println("Window Closing"); System.exit(0); } } public static void main(String[] args) { Frame f = new BadIdea2(); f.setSize(300,200); f.setVisible(true); } } ///:~
Since
actionPerformed( ) is still tightly coupled to the main class,
it’s hard to reuse that code. It’s also messier and less pleasant to
read than the inner class approach.
There’s no reason that you
have to use any of the old thinking for events in Java
1.1 – so why do it?
Another place where you’ll
often see variations on the old way of doing things is when creating a new type
of component. Here’s an example showing that here, too, the new way
works:
//: GoodTechnique.java // Your first choice when overriding components // should be to install listeners. The code is // much safer, more modular and maintainable. import java.awt.*; import java.awt.event.*; class Display { public static final int EVENT = 0, COMPONENT = 1, MOUSE = 2, MOUSE_MOVE = 3, FOCUS = 4, KEY = 5, ACTION = 6, LAST = 7; public String[] evnt; Display() { evnt = new String[LAST]; for(int i = 0; i < LAST; i++) evnt[i] = new String(); } public void show(Graphics g) { for(int i = 0; i < LAST; i++) g.drawString(evnt[i], 0, 10 * i + 10); } } class EnabledPanel extends Panel { Color c; int id; Display display = new Display(); public EnabledPanel(int i, Color mc) { id = i; c = mc; setLayout(new BorderLayout()); add(new MyButton(), BorderLayout.SOUTH); addComponentListener(new CL()); addFocusListener(new FL()); addKeyListener(new KL()); addMouseListener(new ML()); addMouseMotionListener(new MML()); } // To eliminate flicker: public void update(Graphics g) { paint(g); } public void paint(Graphics g) { g.setColor(c); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); g.setColor(Color.black); display.show(g); } // Don't need to enable anything for this: public void processEvent(AWTEvent e) { display.evnt[Display.EVENT]= e.toString(); repaint(); super.processEvent(e); } class CL implements ComponentListener { public void componentMoved(ComponentEvent e){ display.evnt[Display.COMPONENT] = "Component moved"; repaint(); } public void componentResized(ComponentEvent e) { display.evnt[Display.COMPONENT] = "Component resized"; repaint(); } public void componentHidden(ComponentEvent e) { display.evnt[Display.COMPONENT] = "Component hidden"; repaint(); } public void componentShown(ComponentEvent e){ display.evnt[Display.COMPONENT] = "Component shown"; repaint(); } } class FL implements FocusListener { public void focusGained(FocusEvent e) { display.evnt[Display.FOCUS] = "FOCUS gained"; repaint(); } public void focusLost(FocusEvent e) { display.evnt[Display.FOCUS] = "FOCUS lost"; repaint(); } } class KL implements KeyListener { public void keyPressed(KeyEvent e) { display.evnt[Display.KEY] = "KEY pressed: "; showCode(e); } public void keyReleased(KeyEvent e) { display.evnt[Display.KEY] = "KEY released: "; showCode(e); } public void keyTyped(KeyEvent e) { display.evnt[Display.KEY] = "KEY typed: "; showCode(e); } void showCode(KeyEvent e) { int code = e.getKeyCode(); display.evnt[Display.KEY] += KeyEvent.getKeyText(code); repaint(); } } class ML implements MouseListener { public void mouseClicked(MouseEvent e) { requestFocus(); // Get FOCUS on click display.evnt[Display.MOUSE] = "MOUSE clicked"; showMouse(e); } public void mousePressed(MouseEvent e) { display.evnt[Display.MOUSE] = "MOUSE pressed"; showMouse(e); } public void mouseReleased(MouseEvent e) { display.evnt[Display.MOUSE] = "MOUSE released"; showMouse(e); } public void mouseEntered(MouseEvent e) { display.evnt[Display.MOUSE] = "MOUSE entered"; showMouse(e); } public void mouseExited(MouseEvent e) { display.evnt[Display.MOUSE] = "MOUSE exited"; showMouse(e); } void showMouse(MouseEvent e) { display.evnt[Display.MOUSE] += ", x = " + e.getX() + ", y = " + e.getY(); repaint(); } } class MML implements MouseMotionListener { public void mouseDragged(MouseEvent e) { display.evnt[Display.MOUSE_MOVE] = "MOUSE dragged"; showMouse(e); } public void mouseMoved(MouseEvent e) { display.evnt[Display.MOUSE_MOVE] = "MOUSE moved"; showMouse(e); } void showMouse(MouseEvent e) { display.evnt[Display.MOUSE_MOVE] += ", x = " + e.getX() + ", y = " + e.getY(); repaint(); } } } class MyButton extends Button { int clickCounter; String label = ""; public MyButton() { addActionListener(new AL()); } public void paint(Graphics g) { g.setColor(Color.green); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); g.setColor(Color.black); g.drawRect(0, 0, s.width - 1, s.height - 1); drawLabel(g); } private void drawLabel(Graphics g) { FontMetrics fm = g.getFontMetrics(); int width = fm.stringWidth(label); int height = fm.getHeight(); int ascent = fm.getAscent(); int leading = fm.getLeading(); int horizMargin = (getSize().width - width)/2; int verMargin = (getSize().height - height)/2; g.setColor(Color.red); g.drawString(label, horizMargin, verMargin + ascent + leading); } class AL implements ActionListener { public void actionPerformed(ActionEvent e) { clickCounter++; label = "click #" + clickCounter + " " + e.toString(); repaint(); } } } public class GoodTechnique extends Frame { GoodTechnique() { setLayout(new GridLayout(2,2)); add(new EnabledPanel(1, Color.cyan)); add(new EnabledPanel(2, Color.lightGray)); add(new EnabledPanel(3, Color.yellow)); } public static void main(String[] args) { Frame f = new GoodTechnique(); f.setTitle("Good Technique"); f.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e){ System.out.println(e); System.out.println("Window Closing"); System.exit(0); } }); f.setSize(700,700); f.setVisible(true); } } ///:~
This example also demonstrates the
various events that occur and displays the information about them. The class
Display is a way to centralize that information display. There’s an
array of Strings to hold information about each type of event, and the
method show( ) takes a handle to whatever Graphics object you
have and writes directly on that surface. The scheme is intended to be somewhat
reusable.
EnabledPanel represents the
new type of component. It’s a colored panel with a button at the bottom,
and it captures all the events that happen over it by using inner listener
classes for every single event except those in which EnabledPanel
overrides
processEvent( ) in
the old style (notice it must also call super.processEvent( )). The
only reason for using this method is that it captures every event that happens,
so you can view everything that goes on. processEvent( ) does
nothing more than show the string representation of each event, otherwise it
would have to use a cascade of if statements to figure out what event it
was. On the other hand, the inner listener classes already know precisely what
event occurred. (Assuming you register them to components in which you
don’t need any control logic, which should be your goal.) Thus, they
don’t have to check anything out; they just do their
stuff.
Each listener modifies the
Display string associated with its particular event and calls
repaint( ) so the
strings get displayed. You can also see a trick that will usually
eliminate
flicker:
public void update(Graphics g) { paint(g); }
You don’t always need to
override update( ),
but if you write something that flickers, try it. The default version of update
clears the background and then calls paint( ) to redraw any
graphics. This clearing is usually what causes flicker but is not necessary
since paint( ) redraws the entire surface.
You can see that there are a lot of
listeners – however, type checking occurs for the listeners, and you
can’t listen for something that the component doesn’t support
(unlike BadTechnique.java, which you will see
momentarily).
Experimenting with this program is
quite educational since you learn a lot about the way that events occur in Java.
For one thing, it shows a flaw in the design of most windowing systems:
it’s pretty hard to click and release the mouse without moving it, and the
windowing system will often think you’re dragging when you’re
actually just trying to click on something. A solution to this is to use
mousePressed( ) and mouseReleased( ) instead of
mouseClicked( ), and then determine whether to call your own
“mouseReallyClicked( )” method based on time and about 4 pixels
of mouse hysteresis.
The alternative, which you will see
put forward in many published works, is to call
enableEvents( ) and
pass it the masks corresponding to the events you want to handle. This causes
those events to be sent to the old-style methods (although they’re new to
Java 1.1) with names like
processFocusEvent( ). You must also remember to call the base-class
version. Here’s what it looks like:
//: BadTechnique.java // It's possible to override components this way, // but the listener approach is much better, so // why would you? import java.awt.*; import java.awt.event.*; class Display { public static final int EVENT = 0, COMPONENT = 1, MOUSE = 2, MOUSE_MOVE = 3, FOCUS = 4, KEY = 5, ACTION = 6, LAST = 7; public String[] evnt; Display() { evnt = new String[LAST]; for(int i = 0; i < LAST; i++) evnt[i] = new String(); } public void show(Graphics g) { for(int i = 0; i < LAST; i++) g.drawString(evnt[i], 0, 10 * i + 10); } } class EnabledPanel extends Panel { Color c; int id; Display display = new Display(); public EnabledPanel(int i, Color mc) { id = i; c = mc; setLayout(new BorderLayout()); add(new MyButton(), BorderLayout.SOUTH); // Type checking is lost. You can enable and // process events that the component doesn't // capture: enableEvents( // Panel doesn't handle these: AWTEvent.ACTION_EVENT_MASK | AWTEvent.ADJUSTMENT_EVENT_MASK | AWTEvent.ITEM_EVENT_MASK | AWTEvent.TEXT_EVENT_MASK | AWTEvent.WINDOW_EVENT_MASK | // Panel can handle these: AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK | AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK | AWTEvent.CONTAINER_EVENT_MASK); // You can enable an event without // overriding its process method. } // To eliminate flicker: public void update(Graphics g) { paint(g); } public void paint(Graphics g) { g.setColor(c); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); g.setColor(Color.black); display.show(g); } public void processEvent(AWTEvent e) { display.evnt[Display.EVENT]= e.toString(); repaint(); super.processEvent(e); } public void processComponentEvent(ComponentEvent e) { switch(e.getID()) { case ComponentEvent.COMPONENT_MOVED: display.evnt[Display.COMPONENT] = "Component moved"; break; case ComponentEvent.COMPONENT_RESIZED: display.evnt[Display.COMPONENT] = "Component resized"; break; case ComponentEvent.COMPONENT_HIDDEN: display.evnt[Display.COMPONENT] = "Component hidden"; break; case ComponentEvent.COMPONENT_SHOWN: display.evnt[Display.COMPONENT] = "Component shown"; break; default: } repaint(); // Must always remember to call the "super" // version of whatever you override: super.processComponentEvent(e); } public void processFocusEvent(FocusEvent e) { switch(e.getID()) { case FocusEvent.FOCUS_GAINED: display.evnt[Display.FOCUS] = "FOCUS gained"; break; case FocusEvent.FOCUS_LOST: display.evnt[Display.FOCUS] = "FOCUS lost"; break; default: } repaint(); super.processFocusEvent(e); } public void processKeyEvent(KeyEvent e) { switch(e.getID()) { case KeyEvent.KEY_PRESSED: display.evnt[Display.KEY] = "KEY pressed: "; break; case KeyEvent.KEY_RELEASED: display.evnt[Display.KEY] = "KEY released: "; break; case KeyEvent.KEY_TYPED: display.evnt[Display.KEY] = "KEY typed: "; break; default: } int code = e.getKeyCode(); display.evnt[Display.KEY] += KeyEvent.getKeyText(code); repaint(); super.processKeyEvent(e); } public void processMouseEvent(MouseEvent e) { switch(e.getID()) { case MouseEvent.MOUSE_CLICKED: requestFocus(); // Get FOCUS on click display.evnt[Display.MOUSE] = "MOUSE clicked"; break; case MouseEvent.MOUSE_PRESSED: display.evnt[Display.MOUSE] = "MOUSE pressed"; break; case MouseEvent.MOUSE_RELEASED: display.evnt[Display.MOUSE] = "MOUSE released"; break; case MouseEvent.MOUSE_ENTERED: display.evnt[Display.MOUSE] = "MOUSE entered"; break; case MouseEvent.MOUSE_EXITED: display.evnt[Display.MOUSE] = "MOUSE exited"; break; default: } display.evnt[Display.MOUSE] += ", x = " + e.getX() + ", y = " + e.getY(); repaint(); super.processMouseEvent(e); } public void processMouseMotionEvent(MouseEvent e) { switch(e.getID()) { case MouseEvent.MOUSE_DRAGGED: display.evnt[Display.MOUSE_MOVE] = "MOUSE dragged"; break; case MouseEvent.MOUSE_MOVED: display.evnt[Display.MOUSE_MOVE] = "MOUSE moved"; break; default: } display.evnt[Display.MOUSE_MOVE] += ", x = " + e.getX() + ", y = " + e.getY(); repaint(); super.processMouseMotionEvent(e); } } class MyButton extends Button { int clickCounter; String label = ""; public MyButton() { enableEvents(AWTEvent.ACTION_EVENT_MASK); } public void paint(Graphics g) { g.setColor(Color.green); Dimension s = getSize(); g.fillRect(0, 0, s.width, s.height); g.setColor(Color.black); g.drawRect(0, 0, s.width - 1, s.height - 1); drawLabel(g); } private void drawLabel(Graphics g) { FontMetrics fm = g.getFontMetrics(); int width = fm.stringWidth(label); int height = fm.getHeight(); int ascent = fm.getAscent(); int leading = fm.getLeading(); int horizMargin = (getSize().width - width)/2; int verMargin = (getSize().height - height)/2; g.setColor(Color.red); g.drawString(label, horizMargin, verMargin + ascent + leading); } public void processActionEvent(ActionEvent e) { clickCounter++; label = "click #" + clickCounter + " " + e.toString(); repaint(); super.processActionEvent(e); } } public class BadTechnique extends Frame { BadTechnique() { setLayout(new GridLayout(2,2)); add(new EnabledPanel(1, Color.cyan)); add(new EnabledPanel(2, Color.lightGray)); add(new EnabledPanel(3, Color.yellow)); // You can also do it for Windows: enableEvents(AWTEvent.WINDOW_EVENT_MASK); } public void processWindowEvent(WindowEvent e) { System.out.println(e); if(e.getID() == WindowEvent.WINDOW_CLOSING) { System.out.println("Window Closing"); System.exit(0); } } public static void main(String[] args) { Frame f = new BadTechnique(); f.setTitle("Bad Technique"); f.setSize(700,700); f.setVisible(true); } } ///:~
Sure, it works. But it’s ugly
and hard to write, read, debug, maintain, and reuse. So why bother when you can
use inner listener classes?
Java 1.1
has also added some important new functionality, including focus traversal,
desktop color access, printing “inside the sandbox,” and the
beginnings of clipboard support.
Focus
traversal is quite easy, since it’s transparently present in the AWT
library components and you don’t have to do anything to make it work. If
you make your own components and want them to handle focus traversal, you
override
isFocusTraversable( )
to return true. If you want to capture the keyboard focus on a mouse
click, you catch the mouse down event and call
requestFocus( ).
The
desktop
colors provide a way for you to know what the various color choices are on the
current user’s desktop. This way, you can use those colors in your program
if you desire. The colors are automatically initialized and placed in
static members of class SystemColor, so all you need to do is read
the member you’re interested in. The names are intentionally
self-explanatory: desktop, activeCaption,
activeCaptionText, activeCaptionBorder, inactiveCaption,
inactiveCaptionText, inactiveCaptionBorder, window,
windowBorder, windowText, menu, menuText,
text, textText, textHighlight, textHighlightText,
textInactiveText, control, controlText,
controlHighlight, controlLtHighlight, controlShadow,
controlDkShadow, scrollbar, info (for help), and
infoText (for help text).
Unfortunately, there isn’t
much that’s automatic with printing. Instead you must go through a number
of mechanical, non-OO steps in order to print. Printing a component graphically
can be slightly more automatic: by default, the
print( ) method
calls paint( ) to do
its work. There are times when this is satisfactory, but if you want to do
anything more specialized you must know that you’re printing so you can in
particular find out the page dimensions.
The following example demonstrates
the printing of both text and graphics, and the different approaches you can use
for printing graphics. In addition, it tests the printing
support:
//: PrintDemo.java // Printing with Java 1.1 import java.awt.*; import java.awt.event.*; public class PrintDemo extends Frame { Button printText = new Button("Print Text"), printGraphics = new Button("Print Graphics"); TextField ringNum = new TextField(3); Choice faces = new Choice(); Graphics g = null; Plot plot = new Plot3(); // Try different plots Toolkit tk = Toolkit.getDefaultToolkit(); public PrintDemo() { ringNum.setText("3"); ringNum.addTextListener(new RingL()); Panel p = new Panel(); p.setLayout(new FlowLayout()); printText.addActionListener(new TBL()); p.add(printText); p.add(new Label("Font:")); p.add(faces); printGraphics.addActionListener(new GBL()); p.add(printGraphics); p.add(new Label("Rings:")); p.add(ringNum); setLayout(new BorderLayout()); add(p, BorderLayout.NORTH); add(plot, BorderLayout.CENTER); String[] fontList = tk.getFontList(); for(int i = 0; i < fontList.length; i++) faces.add(fontList[i]); faces.select("Serif"); } class PrintData { public PrintJob pj; public int pageWidth, pageHeight; PrintData(String jobName) { pj = getToolkit().getPrintJob( PrintDemo.this, jobName, null); if(pj != null) { pageWidth = pj.getPageDimension().width; pageHeight= pj.getPageDimension().height; g = pj.getGraphics(); } } void end() { pj.end(); } } class ChangeFont { private int stringHeight; ChangeFont(String face, int style,int point){ if(g != null) { g.setFont(new Font(face, style, point)); stringHeight = g.getFontMetrics().getHeight(); } } int stringWidth(String s) { return g.getFontMetrics().stringWidth(s); } int stringHeight() { return stringHeight; } } class TBL implements ActionListener { public void actionPerformed(ActionEvent e) { PrintData pd = new PrintData("Print Text Test"); // Null means print job canceled: if(pd == null) return; String s = "PrintDemo"; ChangeFont cf = new ChangeFont( faces.getSelectedItem(), Font.ITALIC,72); g.drawString(s, (pd.pageWidth - cf.stringWidth(s)) / 2, (pd.pageHeight - cf.stringHeight()) / 3); s = "A smaller point size"; cf = new ChangeFont( faces.getSelectedItem(), Font.BOLD, 48); g.drawString(s, (pd.pageWidth - cf.stringWidth(s)) / 2, (int)((pd.pageHeight - cf.stringHeight())/1.5)); g.dispose(); pd.end(); } } class GBL implements ActionListener { public void actionPerformed(ActionEvent e) { PrintData pd = new PrintData("Print Graphics Test"); if(pd == null) return; plot.print(g); g.dispose(); pd.end(); } } class RingL implements TextListener { public void textValueChanged(TextEvent e) { int i = 1; try { i = Integer.parseInt(ringNum.getText()); } catch(NumberFormatException ex) { i = 1; } plot.rings = i; plot.repaint(); } } public static void main(String[] args) { Frame pdemo = new PrintDemo(); pdemo.setTitle("Print Demo"); pdemo.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); pdemo.setSize(500, 500); pdemo.setVisible(true); } } class Plot extends Canvas { public int rings = 3; } class Plot1 extends Plot { // Default print() calls paint(): public void paint(Graphics g) { int w = getSize().width; int h = getSize().height; int xc = w / 2; int yc = w / 2; int x = 0, y = 0; for(int i = 0; i < rings; i++) { if(x < xc && y < yc) { g.drawOval(x, y, w, h); x += 10; y += 10; w -= 20; h -= 20; } } } } class Plot2 extends Plot { // To fit the picture to the page, you must // know whether you're printing or painting: public void paint(Graphics g) { int w, h; if(g instanceof PrintGraphics) { PrintJob pj = ((PrintGraphics)g).getPrintJob(); w = pj.getPageDimension().width; h = pj.getPageDimension().height; } else { w = getSize().width; h = getSize().height; } int xc = w / 2; int yc = w / 2; int x = 0, y = 0; for(int i = 0; i < rings; i++) { if(x < xc && y < yc) { g.drawOval(x, y, w, h); x += 10; y += 10; w -= 20; h -= 20; } } } } class Plot3 extends Plot { // Somewhat better. Separate // printing from painting: public void print(Graphics g) { // Assume it's a PrintGraphics object: PrintJob pj = ((PrintGraphics)g).getPrintJob(); int w = pj.getPageDimension().width; int h = pj.getPageDimension().height; doGraphics(g, w, h); } public void paint(Graphics g) { int w = getSize().width; int h = getSize().height; doGraphics(g, w, h); } private void doGraphics( Graphics g, int w, int h) { int xc = w / 2; int yc = w / 2; int x = 0, y = 0; for(int i = 0; i < rings; i++) { if(x < xc && y < yc) { g.drawOval(x, y, w, h); x += 10; y += 10; w -= 20; h -= 20; } } } } ///:~
The program allows you to select
fonts from a Choice list (and you’ll see that the number of fonts
available in Java 1.1 is still extremely limited, and
has nothing to do with any extra fonts you install on your machine). It uses
these to print out text in bold, italic, and in different sizes. In addition, a
new type of component called a Plot is created to demonstrate graphics. A
Plot has rings that it will display on the screen and print onto paper,
and the three derived classes Plot1, Plot2, and Plot3
perform these tasks in different ways so that you can see your alternatives when
printing graphics. Also, you can change the number of rings in a plot –
this is interesting because it shows the printing fragility in Java
1.1. On my system, the printer gave error messages and
didn’t print correctly when the ring count got “too high”
(whatever that means), but worked fine when the count was “low
enough.” You will notice, too, that the page dimensions produced when
printing do not seem to correspond to the actual dimensions of the page. This
might be fixed in a future release of Java, and you can use this program to test
it.
This program encapsulates
functionality inside inner classes whenever possible, to facilitate reuse. For
example, whenever you want to begin a print job (whether for graphics or text),
you must create a
PrintJob object, which
has its own Graphics object along with the width and height of the page.
The creation of a PrintJob and extraction of page dimensions is
encapsulated in the PrintData class.
Conceptually,
printing text is
straightforward: you choose a typeface and size, decide where the string should
go on the page, and draw it with Graphics.drawString( ). This means,
however, that you must perform the calculations of exactly where each line will
go on the page to make sure it doesn’t run off the end of the page or
collide with other lines. If you want to make a word processor, your work is cut
out for you.
ChangeFont encapsulates a
little of the process of changing from one font to another by automatically
creating a new Font
object with your desired typeface, style (Font.BOLD or Font.ITALIC
– there’s no support for underline, strikethrough, etc.), and point
size. It also simplifies the calculation of the width and height of a
string.
When you press the “Print
text” button, the TBL listener is activated. You can see that it
goes through two iterations of creating a ChangeFont object and calling
drawString( ) to print out the string in a calculated position,
centered, one-third, and two-thirds down the page, respectively. Notice whether
these calculations produce the expected results. (They didn’t with the
version I used.)
When you press the “Print
graphics” button the GBL listener is activated. The creation of a
PrintData object initializes g, and then you simply call
print( ) for the component you want to print. To force printing you
must call dispose( )
for the Graphics object
and end( ) for the
PrintData object (which turns around and calls end( ) for the
PrintJob).
The work is going on inside the
Plot object. You can see that the base-class Plot is simple
– it extends Canvas and contains an int called rings
to indicate how many concentric rings to draw on this particular Canvas.
The three derived classes show different approaches to accomplishing the same
goal: drawing on both the screen and on the printed page.
Plot1 takes the simplest
approach to coding: ignore the fact that there are differences in painting and
printing, and just override
paint( ). The reason
this works is that the default
print( ) method simply
turns around and calls paint( ). However, you’ll notice that
the size of the output depends on the size of the on-screen canvas, which makes
sense since the width and height are determined by calling
Canvas.getSize( ). The other situation in which this is acceptable
is if your image is always a fixed size.
When the size of the drawing
surface is important, then you must discover the dimensions. Unfortunately, this
turns out to be awkward, as you can see in Plot2. For some possibly good
reason that I don’t know, you cannot simply ask the Graphics object
the dimensions of its drawing surface. This would have made the whole process
quite elegant. Instead, to see if you’re printing rather than painting,
you must detect the
PrintGraphics using the
RTTI instanceof keyword (described in Chapter 11), then downcast and call
the sole PrintGraphics method:
getPrintJob( ). Now
you have a handle to the PrintJob and you can find out the width and
height of the paper. This is a hacky approach, but perhaps there is some
rational reason for it. (On the other hand, you’ve seen some of the other
library designs by now so you might get the impression that the designers were,
in fact, just hacking around...)
You can see that
paint( ) in Plot2 goes through both possibilities of printing
or painting. But since the print( ) method should be called when
printing, why not use that? This approach is used in Plot3, and it
eliminates the need to use instanceof since inside print( )
you can assume that you can cast to a PrintGraphics object. This is a
little better. The situation is improved by placing the common drawing code
(once the dimensions have been detected) inside a separate method
doGraphics( ).
What if you’d like to print
from within an applet? Well, to print anything you must get a PrintJob
object through a Toolkit
object’s
getPrintJob( )
method, which takes only a Frame object and not an Applet. Thus it
would seem that it’s possible to print from within an application, but not
an applet. However, it turns out that you can
create a Frame from
within an applet (which is the reverse of what I’ve been doing for the
applet/application examples so far, which has been making an applet and putting
inside a Frame). This is a useful technique since it allows you to use
many applications within applets (as long as they
don’t violate applet security). When the application window comes up
within an applet, however, you’ll notice that the Web browser sticks a
little caveat on it, something to the effect of “Warning: Applet
Window.”
You can see that it’s quite
straightforward to put a Frame inside an applet. The only thing that you
must add is code to
dispose( ) of the
Frame when the user closes it (instead of calling
System.exit( )):
//: PrintDemoApplet.java // Creating a Frame from within an Applet import java.applet.*; import java.awt.*; import java.awt.event.*; public class PrintDemoApplet extends Applet { public void init() { Button b = new Button("Run PrintDemo"); b.addActionListener(new PDL()); add(b); } class PDL implements ActionListener { public void actionPerformed(ActionEvent e) { final PrintDemo pd = new PrintDemo(); pd.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ pd.dispose(); } }); pd.setSize(500, 500); pd.show(); } } } ///:~
There’s some confusion
involved with Java 1.1
printing support. Some of the
publicity seemed to claim that you’d be able to print from within an
applet. However, the Java security system contains a feature that could lock out
an applet from initiating its own print job, requiring that the initiation be
done via a Web browser or applet viewer. At the time of this writing, this
seemed to remain an unresolved issue. When I ran this program from within a Web
browser, the PrintDemo window came up just fine, but it wouldn’t
print from the browser.
Java 1.1
supports limited operations with the
system
clipboard (in the java.awt.datatransfer package). You can copy
String objects to the clipboard as text, and you can paste text from the
clipboard into String objects. Of course, the clipboard is designed to
hold any type of data, but how this data is represented on the clipboard is up
to the program doing the cutting and pasting. Although it currently supports
only string data, the Java clipboard API provides for extensibility through the
concept of a “flavor.” When data comes off the clipboard, it has an
associated set of flavors that
it can be converted to (for example, a graph might be represented as a string of
numbers or as an image) and you can see if that particular clipboard data
supports the flavor you’re interested in.
The following program is a simple
demonstration of cut, copy, and paste with String data in a
TextArea. One thing
you’ll notice is that the keyboard sequences you normally use for cutting,
copying, and pasting also work. But if you look at any TextField or
TextArea in any other program you’ll find that they also
automatically support the clipboard key sequences. This example simply adds
programmatic control of the clipboard, and you could use these techniques if you
want to capture clipboard text into some
non-TextComponent.
//: CutAndPaste.java // Using the clipboard from Java 1.1 import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; public class CutAndPaste extends Frame { MenuBar mb = new MenuBar(); Menu edit = new Menu("Edit"); MenuItem cut = new MenuItem("Cut"), copy = new MenuItem("Copy"), paste = new MenuItem("Paste"); TextArea text = new TextArea(20,20); Clipboard clipbd = getToolkit().getSystemClipboard(); public CutAndPaste() { cut.addActionListener(new CutL()); copy.addActionListener(new CopyL()); paste.addActionListener(new PasteL()); edit.add(cut); edit.add(copy); edit.add(paste); mb.add(edit); setMenuBar(mb); add(text, BorderLayout.CENTER); } class CopyL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString, clipString); } } class CutL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString, clipString); text.replaceRange("", text.getSelectionStart(), text.getSelectionEnd()); } } class PasteL implements ActionListener { public void actionPerformed(ActionEvent e) { Transferable clipData = clipbd.getContents(CutAndPaste.this); try { String clipString = (String)clipData. getTransferData( DataFlavor.stringFlavor); text.replaceRange(clipString, text.getSelectionStart(), text.getSelectionEnd()); } catch(Exception ex) { System.out.println("not String flavor"); } } } public static void main(String[] args) { CutAndPaste cp = new CutAndPaste(); cp.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); cp.setSize(300,200); cp.setVisible(true); } } ///:~
The creation and addition of the
menu and TextArea should by now seem a pedestrian activity. What’s
different is the creation of the Clipboard field clipbd, which is
done through the Toolkit.
All the action takes place in the
listeners. The CopyL and CutL listeners are the same except for
the last line of CutL, which erases the line that’s been copied.
The special two lines are the creation of a
StringSelection object
from the String and the call to
setContents( ) with
this StringSelection. That’s all there is to putting a
String on the clipboard.
In PasteL, data is pulled
off the clipboard using
getContents( ). What
comes back is a fairly anonymous
Transferable object, and
you don’t really know what it contains. One way to find out is to call
getTransferDataFlavors( ),
which returns an array of
DataFlavor objects
indicating which flavors are supported by this particular object. You can also
ask it directly with
isDataFlavorSupported( ),
passing in the flavor you’re interested in. Here, however, the bold
approach is taken:
getTransferData( )
is called assuming that the contents supports the String flavor, and if
it doesn’t the problem is sorted out in the exception
handler.
So far in this book you’ve
seen how valuable Java is for creating
reusable pieces of code. The “most reusable”
unit of code has been the class, since it comprises a cohesive unit of
characteristics (fields) and behaviors (methods) that can be reused either
directly via composition or through inheritance.
Inheritance and polymorphism are
essential parts of object-oriented programming, but in the majority of cases
when you’re putting together an application, what you really want is
components that do exactly what you need. You’d like to drop these parts
into your design like the electronic engineer puts together chips on a circuit
board (or even, in the case of Java, onto a Web page). It seems, too, that there
should be some way to accelerate this “modular assembly” style of
programming.
“Visual
programming” first became successful – very successful
– with
Microsoft’s
Visual Basic (VB), followed by a second-generation design in
Borland’s
Delphi (the primary inspiration for the Java Beans design). With these
programming tools the components are represented visually, which makes sense
since they usually display some kind of visual component such as a button or a
text field. The visual representation, in fact, is often exactly the way the
component will look in the running program. So part of the process of visual
programming involves dragging a component from a pallet and dropping it onto
your form. The application
builder tool writes code as you do this, and that code will cause the component
to be created in the running program.
Simply dropping the component onto
a form is usually not enough to complete the program. Often, you must change the
characteristics of a component, such as what color it is, what text is on it,
what database it’s connected to, etc. Characteristics that can be modified
at design time are referred to as
properties. You can
manipulate the properties of your component inside the application builder tool,
and when you create the program this configuration data is saved so that it can
be rejuvenated when the program is started.
By now you’re probably used
to the idea that an object is more than characteristics; it’s also a set
of behaviors. At design-time, the behaviors of a visual component are partially
represented by events,
meaning “Here’s something that can happen to the component.”
Ordinarily, you decide what you want to happen when an event occurs by tying
code to that event.
Here’s the critical part: the
application builder tool is able to dynamically interrogate (using
reflection) the component to
find out which properties and events the component supports. Once it knows what
they are, it can display the properties and allow you to change those (saving
the state when you build the program), and also display the events. In general,
you do something like double clicking on an event and the application builder
tool creates a code body and ties it to that particular event. All you have to
do at that point is write the code that executes when the event
occurs.
All this adds up to a lot of work
that’s done for you by the application builder tool. As a result you can
focus on what the program looks like and what it is supposed to do, and rely on
the application builder tool to manage the connection details for you. The
reason that visual programming tools have been so successful is that they
dramatically speed up the process of building an application – certainly
the user interface, but often other portions of the application as
well.
After the dust settles, then, a
component is really just a block
of code, typically embodied in a class. The key issue is the ability for the
application builder tool to discover the properties and events for that
component. To create a VB component, the programmer had to write a fairly
complicated piece of code following certain conventions to expose the properties
and events. Delphi was a second-generation visual programming tool and the
language was actively designed around visual programming so it is much easier to
create a visual component. However, Java has brought the creation of visual
components to its most advanced state with Java Beans, because a Bean is just a
class. You don’t have to write any extra code or use special language
extensions in order to make something a Bean. The only thing you need to do, in
fact, is slightly modify the way that you name your methods. It is the method
name that tells the application builder tool whether this is a property, an
event, or just an ordinary method.
In the Java documentation, this
naming convention is mistakenly termed a “design pattern.” This is
unfortunate since design patterns (see Chapter 16) are challenging enough
without this sort of confusion. It’s not a design pattern, it’s just
a naming convention and it’s fairly
simple:
Point 1
above answers a question about something you might have noticed in the change
from Java 1.0 to Java 1.1: a
number of method names have had small, apparently meaningless name changes. Now
you can see that most of those changes had to do with adapting to the
“get” and “set” naming conventions in order to make that
particular component into a Bean.
We can use these guidelines to
create a simple Bean:
//: Frog.java // A trivial Java Bean package frogbean; import java.awt.*; import java.awt.event.*; class Spots {} public class Frog { private int jumps; private Color color; private Spots spots; private boolean jmpr; public int getJumps() { return jumps; } public void setJumps(int newJumps) { jumps = newJumps; } public Color getColor() { return color; } public void setColor(Color newColor) { color = newColor; } public Spots getSpots() { return spots; } public void setSpots(Spots newSpots) { spots = newSpots; } public boolean isJumper() { return jmpr; } public void setJumper(boolean j) { jmpr = j; } public void addActionListener( ActionListener l) { //... } public void removeActionListener( ActionListener l) { // ... } public void addKeyListener(KeyListener l) { // ... } public void removeKeyListener(KeyListener l) { // ... } // An "ordinary" public method: public void croak() { System.out.println("Ribbet!"); } } ///:~
First, you can see that it’s
just a class. Usually, all your fields will be private, and accessible
only through methods. Following the naming convention, the properties are
jumps, color, spots, and jumper (notice the change
in case of the first letter in the property name). Although the name of the
internal identifier is the same as the name of the property in the first three
cases, in jumper you can see that the property name does not force you to
use any particular name for internal variables (or, indeed, to even have
any internal variable for that property).
The events this Bean handles are
ActionEvent and KeyEvent, based on the naming of the
“add” and “remove” methods for the associated listener.
Finally, you can see that the ordinary method croak( ) is still part
of the Bean simply because it’s a public method, not because it
conforms to any naming scheme.
One of the most critical parts of
the Bean scheme occurs when you drag a Bean off a palette and plop it down on a
form. The application builder tool must be able to create the Bean (which it can
do if there’s a default constructor) and then, without access to the
Bean’s source code, extract all the necessary information to create the
property sheet and event handlers.
Part of the solution is already
evident from the end of Chapter 11: Java 1.1
reflection allows all the
methods of an anonymous class to be discovered. This is perfect for solving the
Bean problem without requiring you to use any extra language keywords like those
required in other visual programming languages. In fact, one of the prime
reasons that reflection was added to Java 1.1 was to support Beans (although
reflection also supports object serialization and remote method invocation). So
you might expect that the creator of the application builder tool would have to
reflect each Bean and hunt through its methods to find the properties and events
for that Bean.
This is certainly possible, but the
Java designers wanted to provide a standard interface for everyone to use, not
only to make Beans simpler to use but also to provide a standard gateway to the
creation of more complex Beans. This interface is the
Introspector class, and
the most important method in this class is the static
getBeanInfo( ). You
pass a Class handle to this method and it fully interrogates that class
and returns a BeanInfo object that you can then dissect to find
properties, methods, and events.
Usually you won’t care about
any of this – you’ll probably get most of your Beans off the shelf
from vendors, and you don’t need to know all the magic that’s going
on underneath. You’ll simply drag your Beans onto your form, then
configure their properties and write handlers for the events you’re
interested in. However, it’s an interesting and educational exercise to
use the Introspector to display information about a Bean, so here’s
a tool that does it (you’ll find it in the frogbean
subdirectory):
//: BeanDumper.java // A method to introspect a Bean import java.beans.*; import java.lang.reflect.*; public class BeanDumper { public static void dump(Class bean){ BeanInfo bi = null; try { bi = Introspector.getBeanInfo( bean, java.lang.Object.class); } catch(IntrospectionException ex) { System.out.println("Couldn't introspect " + bean.getName()); System.exit(1); } PropertyDescriptor[] properties = bi.getPropertyDescriptors(); for(int i = 0; i < properties.length; i++) { Class p = properties[i].getPropertyType(); System.out.println( "Property type:\n " + p.getName()); System.out.println( "Property name:\n " + properties[i].getName()); Method readMethod = properties[i].getReadMethod(); if(readMethod != null) System.out.println( "Read method:\n " + readMethod.toString()); Method writeMethod = properties[i].getWriteMethod(); if(writeMethod != null) System.out.println( "Write method:\n " + writeMethod.toString()); System.out.println("===================="); } System.out.println("Public methods:"); MethodDescriptor[] methods = bi.getMethodDescriptors(); for(int i = 0; i < methods.length; i++) System.out.println( methods[i].getMethod().toString()); System.out.println("======================"); System.out.println("Event support:"); EventSetDescriptor[] events = bi.getEventSetDescriptors(); for(int i = 0; i < events.length; i++) { System.out.println("Listener type:\n " + events[i].getListenerType().getName()); Method[] lm = events[i].getListenerMethods(); for(int j = 0; j < lm.length; j++) System.out.println( "Listener method:\n " + lm[j].getName()); MethodDescriptor[] lmd = events[i].getListenerMethodDescriptors(); for(int j = 0; j < lmd.length; j++) System.out.println( "Method descriptor:\n " + lmd[j].getMethod().toString()); Method addListener = events[i].getAddListenerMethod(); System.out.println( "Add Listener Method:\n " + addListener.toString()); Method removeListener = events[i].getRemoveListenerMethod(); System.out.println( "Remove Listener Method:\n " + removeListener.toString()); System.out.println("===================="); } } // Dump the class of your choice: public static void main(String[] args) { if(args.length < 1) { System.err.println("usage: \n" + "BeanDumper fully.qualified.class"); System.exit(0); } Class c = null; try { c = Class.forName(args[0]); } catch(ClassNotFoundException ex) { System.err.println( "Couldn't find " + args[0]); System.exit(0); } dump(c); } } ///:~
BeanDumper.dump( ) is
the method that does all the work. First it tries to create a BeanInfo
object, and if successful calls the methods of BeanInfo that produce
information about properties, methods, and events. In
Introspector.getBeanInfo( ), you’ll see there is a second
argument. This tells the Introspector where to stop in the inheritance
hierarchy. Here, it stops before it parses all the methods from Object,
since we’re not interested in seeing those.
For properties,
getPropertyDescriptors( )
returns an array of
PropertyDescriptors. For
each PropertyDescriptor you can call
getPropertyType( )
to find the class of object that is passed in and out via the property methods.
Then, for each property you can get its pseudonym (extracted from the method
names) with
getName( ), the
method for reading with
getReadMethod( ),
and the method for writing with
getWriteMethod( ).
These last two methods return a Method object that can actually be used
to invoke the corresponding method on the object (this is part of
reflection).
For the public methods (including
the property methods),
getMethodDescriptors( )
returns an array of
MethodDescriptors. For
each one you can get the associated
Method object and print
out its name.
For the events,
getEventSetDescriptors( )
returns an array of (what else?)
EventSetDescriptors. Each
of these can be queried to find out the class of the listener, the methods of
that listener class, and the add- and remove-listener methods. The BeanDumper
program prints out all of this information.
If you invoke BeanDumper on
the Frog class like this:
java BeanDumper frogbean.Frog
the output, after removing extra
details that are unnecessary here, is:
class name: Frog Property type: Color Property name: color Read method: public Color getColor() Write method: public void setColor(Color) ==================== Property type: Spots Property name: spots Read method: public Spots getSpots() Write method: public void setSpots(Spots) ==================== Property type: boolean Property name: jumper Read method: public boolean isJumper() Write method: public void setJumper(boolean) ==================== Property type: int Property name: jumps Read method: public int getJumps() Write method: public void setJumps(int) ==================== Public methods: public void setJumps(int) public void croak() public void removeActionListener(ActionListener) public void addActionListener(ActionListener) public int getJumps() public void setColor(Color) public void setSpots(Spots) public void setJumper(boolean) public boolean isJumper() public void addKeyListener(KeyListener) public Color getColor() public void removeKeyListener(KeyListener) public Spots getSpots() ====================== Event support: Listener type: KeyListener Listener method: keyTyped Listener method: keyPressed Listener method: keyReleased Method descriptor: public void keyTyped(KeyEvent) Method descriptor: public void keyPressed(KeyEvent) Method descriptor: public void keyReleased(KeyEvent) Add Listener Method: public void addKeyListener(KeyListener) Remove Listener Method: public void removeKeyListener(KeyListener) ==================== Listener type: ActionListener Listener method: actionPerformed Method descriptor: public void actionPerformed(ActionEvent) Add Listener Method: public void addActionListener(ActionListener) Remove Listener Method: public void removeActionListener(ActionListener) ====================
This reveals most of what the
Introspector sees as it produces a BeanInfo object from your Bean.
You can see that the type of the property and its name are independent. Notice
the lowercasing of the property name. (The only time this doesn’t occur is
when the property name begins with more than one capital letter in a row.) And
remember that the method names you’re seeing here (such as the read and
write methods) are actually produced from a Method object that can be
used to invoke the associated method on the object.
The public method list includes the
methods that are not associated with a property or event, such as
croak( ), as well as those that are. These are all the methods that
you can call programmatically for a Bean, and the application builder tool can
choose to list all of these while you’re making method calls, to ease your
task.
Finally, you can see that the
events are fully parsed out into the listener, its methods, and the add- and
remove-listener methods. Basically, once you have the BeanInfo, you can
find out everything of importance for the Bean. You can also call the methods
for that Bean, even though you don’t have any other information except the
object (again, a feature of
reflection).
This next example is slightly more
sophisticated, albeit frivolous. It’s a canvas that draws a little circle
around the mouse whenever the mouse is moved. When you press the mouse, the word
“Bang!” appears in the middle of the screen, and an action listener
is fired.
The properties you can change are
the size of the circle as well as the color, size, and text of the word that is
displayed when you press the mouse. A BangBean also has its own
addActionListener( )
and
removeActionListener( )
so you can attach your own listener that will be fired when the user clicks on
the BangBean. You should be able to recognize the property and event
support:
//: BangBean.java // A graphical Bean package bangbean; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; public class BangBean extends Canvas implements Serializable { protected int xm, ym; protected int cSize = 20; // Circle size protected String text = "Bang!"; protected int fontSize = 48; protected Color tColor = Color.red; protected ActionListener actionListener; public BangBean() { addMouseListener(new ML()); addMouseMotionListener(new MML()); } public int getCircleSize() { return cSize; } public void setCircleSize(int newSize) { cSize = newSize; } public String getBangText() { return text; } public void setBangText(String newText) { text = newText; } public int getFontSize() { return fontSize; } public void setFontSize(int newSize) { fontSize = newSize; } public Color getTextColor() { return tColor; } public void setTextColor(Color newColor) { tColor = newColor; } public void paint(Graphics g) { g.setColor(Color.black); g.drawOval(xm - cSize/2, ym - cSize/2, cSize, cSize); } // This is a unicast listener, which is // the simplest form of listener management: public void addActionListener ( ActionListener l) throws TooManyListenersException { if(actionListener != null) throw new TooManyListenersException(); actionListener = l; } public void removeActionListener( ActionListener l) { actionListener = null; } class ML extends MouseAdapter { public void mousePressed(MouseEvent e) { Graphics g = getGraphics(); g.setColor(tColor); g.setFont( new Font( "TimesRoman", Font.BOLD, fontSize)); int width = g.getFontMetrics().stringWidth(text); g.drawString(text, (getSize().width - width) /2, getSize().height/2); g.dispose(); // Call the listener's method: if(actionListener != null) actionListener.actionPerformed( new ActionEvent(BangBean.this, ActionEvent.ACTION_PERFORMED, null)); } } class MML extends MouseMotionAdapter { public void mouseMoved(MouseEvent e) { xm = e.getX(); ym = e.getY(); repaint(); } } public Dimension getPreferredSize() { return new Dimension(200, 200); } // Testing the BangBean: public static void main(String[] args) { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) {} Frame aFrame = new Frame("BangBean Test"); aFrame.addWindowListener( new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); aFrame.add(bb, BorderLayout.CENTER); aFrame.setSize(300,300); aFrame.setVisible(true); } // During testing, send action information // to the console: static class BBL implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("BangBean action"); } } } ///:~
The first thing you’ll notice
is that BangBean implements the
Serializable interface.
This means that the application builder tool can “pickle” all the
information for the BangBean using serialization after the program
designer has adjusted the values of the properties. When the Bean is created as
part of the running application, these “pickled” properties are
restored so that you get exactly what you designed.
You can see that all the fields are
private, which is what you’ll usually do with a Bean – allow
access only through methods, usually using the “property”
scheme.
When you look at the signature for
addActionListener( ), you’ll see that it can throw a
TooManyListenersException.
This indicates that it is
unicast, which means it
notifies only one listener when the event occurs. Ordinarily, you’ll use
multicast events so that
many listeners can be notified of an event. However, that runs into issues that
you won’t be ready for until the next chapter, so it will be revisited
there (under the heading “Java Beans revisited”). A unicast event
sidesteps the problem.
When you press the mouse, the text
is put in the middle of the BangBean, and if the actionListener
field is not null, its actionPerformed( ) is called, creating
a new ActionEvent object
in the process. Whenever the mouse is moved, its new coordinates are captured
and the canvas is repainted (erasing any text that’s on the canvas, as
you’ll see).
The main( ) is added to
allow you to test the program from the command line. When a Bean is in a
development environment, main( ) will not be used, but it’s
helpful to have a main( ) in each of your Beans because it provides
for rapid testing. main( ) creates a Frame and places a
BangBean within it, attaching a simple ActionListener to the
BangBean to print to the console whenever an ActionEvent occurs.
Usually, of course, the application builder tool would create most of the code
that uses the Bean.
When you run the BangBean
through BeanDumper or put the BangBean inside a Bean-enabled
development environment, you’ll notice that there are many more properties
and actions than are evident from the above code. That’s because
BangBean is inherited from Canvas, and Canvas is a Bean, so
you’re seeing its properties and events as
well.
Before you can bring a Bean into a
Bean-enabled visual builder tool, it must be put into the standard Bean
container, which is a JAR (Java
ARchive) file that includes all the Bean classes as well as a
“manifest” file that says “This is a Bean.” A manifest
file is simply a text file that follows a particular form. For the
BangBean, the manifest file looks like this:
Manifest-Version: 1.0 Name: bangbean/BangBean.class Java-Bean: True
The first line indicates the
version of the manifest scheme, which until further notice from Sun is 1.0. The
second line (empty lines are ignored) names the BangBean.class file, and
the third says, “It’s a Bean.” Without the third line, the
program builder tool will not recognize the class as a Bean.
The only tricky part is that you
must make sure that you get the proper path in the “Name:” field. If
you look back at BangBean.java, you’ll see it’s in package
bangbean (and thus in a subdirectory called “bangbean”
that’s off of the classpath), and the name in the manifest file must
include this package information. In addition, you must place the manifest file
in the directory above the root of your package path, which in this case
means placing the file in the directory above the “bangbean”
subdirectory. Then you must invoke jar from the same directory as the
manifest file, as
follows:
jar cfm BangBean.jar BangBean.mf bangbean
This assumes that you want the
resulting JAR file to be named BangBean.jar and that you’ve put the
manifest in a file called BangBean.mf.
You might wonder “What about
all the other classes that were generated when I compiled
BangBean.java?” Well, they all ended up inside the bangbean
subdirectory, and you’ll see that the last argument for the above
jar command line is the bangbean subdirectory. When you give
jar the name of a subdirectory, it packages that entire subdirectory into
the jar file (including, in this case, the original BangBean.java
source-code file – you might not choose to include the source with your
own Beans). In addition, if you turn around and unpack the JAR file you’ve
just created, you’ll discover that your manifest file isn’t inside,
but that jar has created its own manifest file (based partly on yours)
called MANIFEST.MF and placed it inside the subdirectory META-INF
(for “meta-information”). If you open this manifest file
you’ll also notice that digital signature information has been added by
jar for each file, of the form:
Digest-Algorithms: SHA MD5 SHA-Digest: pDpEAG9NaeCx8aFtqPI4udSX/O0= MD5-Digest: O4NcS1hE3Smnzlp2hj6qeg==
In general, you don’t need to
worry about any of this, and if you make changes you can just modify your
original manifest file and re-invoke jar to create a new JAR file for
your Bean. You can also add other Beans to the JAR file simply by adding their
information to your manifest.
One thing to notice is that
you’ll probably want to put each Bean in its own subdirectory, since when
you create a JAR file you hand the jar utility the name of a subdirectory
and it puts everything in that subdirectory into the JAR file. You can see that
both Frog and BangBean are in their own
subdirectories.
Once you have your Bean properly
inside a JAR file you can bring it into a Beans-enabled program-builder
environment. The way you do this varies from one tool to the next, but Sun
provides a freely-available test bed for Java Beans in their “Beans
Development Kit” (BDK) called the
“beanbox.” (Download
the BDK from www.javasoft.com.) To place your Bean in the beanbox, copy
the JAR file into the BDK’s “jars” subdirectory before you
start up the beanbox.
You can see how remarkably simple
it is to make a Bean. But you aren’t limited to what you’ve seen
here. The Java Bean design provides a simple point of entry but can also scale
to more complex situations. These situations are beyond the scope of this book
but they will be briefly introduced here. You can find more details at
http://java.sun.com/beans.
One place where you can add
sophistication is with properties. The examples above have shown only single
properties, but it’s also possible to represent multiple properties in an
array. This is called an
indexed
property. You simply provide the appropriate methods (again following a
naming convention for the method names) and the Introspector recognizes
an indexed property so your application builder tool can respond
appropriately.
Properties can be
bound,
which means that they will notify other objects via a
PropertyChangeEvent. The other objects can then choose to change
themselves based on the change to the Bean.
Properties can be
constrained,
which means that other objects can veto a change to that property if it is
unacceptable. The other objects are notified using a
PropertyChangeEvent, and
they can throw a
ProptertyVetoException to
prevent the change from happening and to restore the old
values.
There’s another issue that
couldn’t be addressed here. Whenever you create a Bean, you should expect
that it will be run in a multithreaded environment. This means that you must
understand the issues of threading, which will be introduced in the next
chapter. You’ll find a section there called “Java Beans
revisited” that will look at the problem and its
solution.
After working your way through this
chapter and seeing the huge changes that have occurred within the AWT (although,
if you can remember back that far, Sun claimed Java was a “stable”
language when it first appeared), you might still have the feeling that
it’s not quite done. Sure, there’s now a good event model, and
JavaBeans is an excellent component-reuse design. But the GUI components still
seem rather minimal, primitive, and awkward.
That’s where
Swing comes in. The Swing library appeared after Java
1.1 so you might naturally assume that it’s part of
Java 1.2. However, it is designed to work with
Java 1.1 as an add-on. This way, you don’t have to
wait for your platform to support Java 1.2 in order to enjoy a good UI component
library. Your users might actually need to download the Swing library if it
isn’t part of their Java 1.1 support, and this could cause a few snags.
But it works.
Swing contains all the components
that you’ve been missing throughout the rest of this chapter: those you
expect to see in a modern UI, everything from buttons that contain pictures to
trees and grids. It’s a big library, but it’s designed to have
appropriate complexity for the task at hand – if something is simple, you
don’t have to write much code but as you try to do more your code becomes
increasingly complex. This means an easy entry point, but you’ve got the
power if you need it.
Swing has great depth. This section
does not attempt to be comprehensive, but instead introduces the power and
simplicity of Swing to get you started using the library. Please be aware that
what you see here is intended to be simple. If you need to do more, then Swing
can probably give you what you want if you’re willing to do the research
by hunting through the online documentation from
Sun.
When you begin to use the Swing
library, you’ll see that it’s a huge step forward. Swing components
are Beans (and thus use the Java 1.1 event model), so they can be used in any
development environment that supports Beans. Swing provides a full set of UI
components. For speed, all the components are
lightweight (no “peer” components are used), and Swing is written
entirely in Java for portability.
Much of what you’ll like
about Swing could be called “orthogonality of use;” that is, once
you pick up the general ideas about the library you can apply them everywhere.
Primarily because of the Beans naming conventions, much of the time I was
writing these examples I could guess at the method names and get it right the
first time, without looking anything up. This is certainly the hallmark of a
good library design. In addition, you can generally plug components into other
components and things will work correctly.
Keyboard
navigation is automatic – you can use a Swing application without the
mouse, but you don’t have to do any extra programming (the old AWT
required some ugly code to achieve keyboard navigation). Scrolling support is
effortless – you simply wrap your component in a
JScrollPane as you add it
to your form. Other features such as tool tips typically require a single line
of code to implement.
Swing also supports something
called “pluggable look and feel,” which means that the appearance of
the UI can be dynamically changed to suit the expectations of users working
under different platforms and operating systems. It’s even possible to
invent your own look and feel.
If you’ve struggled long and
hard to build your UI using Java 1.1, you don’t want to throw it away to
convert to Swing. Fortunately, the library is designed to allow easy conversion
– in many cases you can simply put a ‘J’ in front of the class
names of each of your old AWT components. Here’s an example that should
have a familiar flavor to it:
//: JButtonDemo.java // Looks like Java 1.1 but with J's added package c13.swing; import java.awt.*; import java.awt.event.*; import java.applet.*; import javax.swing.*; public class JButtonDemo extends Applet { JButton b1 = new JButton("JButton 1"), b2 = new JButton("JButton 2"); JTextField t = new JTextField(20); public void init() { ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); t.setText(name + " Pressed"); } }; b1.addActionListener(al); add(b1); b2.addActionListener(al); add(b2); add(t); } public static void main(String args[]) { JButtonDemo applet = new JButtonDemo(); JFrame frame = new JFrame("TextAreaNew"); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ System.exit(0); } }); frame.getContentPane().add( applet, BorderLayout.CENTER); frame.setSize(300,100); applet.init(); applet.start(); frame.setVisible(true); } } ///:~
There’s a new import
statement, but everything else looks like the Java 1.1 AWT with the addition of
some J’s. Also, you don’t just add( ) something to a
Swing JFrame, but you must get the “content pane” first, as
seen above. But you can easily get many of the benefits of Swing with a
simple conversion.
Because of the package
statement, you’ll have to invoke this program by saying:
java c13.swing.JbuttonDemo
Although the programs that are both
applets and applications can be valuable, if used everywhere they become
distracting and waste paper. Instead, a display framework will be used for the
Swing examples in the rest of this section:
//: Show.java // Tool for displaying Swing demos package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Show { public static void inFrame(JPanel jp, int width, int height) { String title = jp.getClass().toString(); // Remove the word "class": if(title.indexOf("class") != -1) title = title.substring(6); JFrame frame = new JFrame(title); frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ System.exit(0); } }); frame.getContentPane().add( jp, BorderLayout.CENTER); frame.setSize(width, height); frame.setVisible(true); } } ///:~
Classes that want to display
themselves should inherit from JPanel and then add any visual components
to themselves. Finally, they create a main( ) containing the
line:
Show.inFrame(new MyClass(), 500, 300);
in which the last two arguments are
the display width and height.
Almost all of the classes that
you’ll be using to create your user interfaces are derived from
JComponent, which
contains a method called
setToolTipText(String).
So, for virtually anything you place on your form, all you need to do is say
(for an object jc of any JComponent-derived
class):
jc.setToolTipText("My tip");
and when the mouse stays over that
JComponent for a predetermined period of time, a tiny box containing your
text will pop up next to the mouse.
JComponent also contains a
method called
setBorder( ), which
allows you to place various interesting borders on any visible component. The
following example demonstrates a number of the different borders that are
available, using a method called showBorder( ) that creates a
JPanel and puts on the border in each case. Also, it uses RTTI to find
the name of the border that you’re using (stripping off all the path
information), then puts that name in a
JLabel in the middle of
the panel:
//: Borders.java // Different Swing borders package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; public class Borders extends JPanel { static JPanel showBorder(Border b) { JPanel jp = new JPanel(); jp.setLayout(new BorderLayout()); String nm = b.getClass().toString(); nm = nm.substring(nm.lastIndexOf('.') + 1); jp.add(new JLabel(nm, JLabel.CENTER), BorderLayout.CENTER); jp.setBorder(b); return jp; } public Borders() { setLayout(new GridLayout(2,4)); add(showBorder(new TitledBorder("Title"))); add(showBorder(new EtchedBorder())); add(showBorder(new LineBorder(Color.blue))); add(showBorder( new MatteBorder(5,5,30,30,Color.green))); add(showBorder( new BevelBorder(BevelBorder.RAISED))); add(showBorder( new SoftBevelBorder(BevelBorder.LOWERED))); add(showBorder(new CompoundBorder( new EtchedBorder(), new LineBorder(Color.red)))); } public static void main(String args[]) { Show.inFrame(new Borders(), 500, 300); } } ///:~
Most of the examples in this
section use TitledBorder,
but you can see that the rest of the borders are as easy to use. You can also
create your own borders and put them inside buttons, labels, etc. –
anything derived from
JComponent.
Swing adds a number of different
types of buttons, and it also changes the organization of the selection
components: all buttons, checkboxes, radio buttons, and even menu items are
inherited from
AbstractButton (which,
since menu items are included, would probably have been better named
“AbstractChooser” or something equally general). You’ll see
the use of menu items shortly, but the following example shows the various types
of buttons available:
//: Buttons.java // Various Swing buttons package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.plaf.basic.*; import javax.swing.border.*; public class Buttons extends JPanel { JButton jb = new JButton("JButton"); BasicArrowButton up = new BasicArrowButton( BasicArrowButton.NORTH), down = new BasicArrowButton( BasicArrowButton.SOUTH), right = new BasicArrowButton( BasicArrowButton.EAST), left = new BasicArrowButton( BasicArrowButton.WEST); public Buttons() { add(jb); add(new JToggleButton("JToggleButton")); add(new JCheckBox("JCheckBox")); add(new JRadioButton("JRadioButton")); JPanel jp = new JPanel(); jp.setBorder(new TitledBorder("Directions")); jp.add(up); jp.add(down); jp.add(left); jp.add(right); add(jp); } public static void main(String args[]) { Show.inFrame(new Buttons(), 300, 200); } } ///:~
The
JButton looks like the
AWT button, but there’s more you can do to it (like add images, as
you’ll see later). In javax.swing.plaf.basic, there is also a
BasicArrowButton that is
convenient.
When you run the example,
you’ll see that the toggle button holds its last position, in or out. But
the check boxes and radio buttons behave identically to each other, just
clicking on or off (they are inherited from
JToggleButton).
If you want radio buttons to behave
in an “exclusive or” fashion, you must add them to a button group,
in a similar but less awkward way as the old AWT. But as the example below
demonstrates, any AbstractButton can be added to a
ButtonGroup.
To avoid repeating a lot of code,
this example uses reflection to generate the groups of different types of
buttons. This is seen in makeBPanel, which creates a button group and a
JPanel, and for each String in the array that’s the second
argument to makeBPanel( ), it adds an object of the class
represented by the first argument:
//: ButtonGroups.java // Uses reflection to create groups of different // types of AbstractButton. package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import java.lang.reflect.*; public class ButtonGroups extends JPanel { static String[] ids = { "June", "Ward", "Beaver", "Wally", "Eddie", "Lumpy", }; static JPanel makeBPanel(Class bClass, String[] ids) { ButtonGroup bg = new ButtonGroup(); JPanel jp = new JPanel(); String title = bClass.getName(); title = title.substring( title.lastIndexOf('.') + 1); jp.setBorder(new TitledBorder(title)); for(int i = 0; i < ids.length; i++) { AbstractButton ab = new JButton("failed"); try { // Get the dynamic constructor method // that takes a String argument: Constructor ctor = bClass.getConstructor( new Class[] { String.class }); // Create a new object: ab = (AbstractButton)ctor.newInstance( new Object[]{ids[i]}); } catch(Exception ex) { System.out.println("can't create " + bClass); } bg.add(ab); jp.add(ab); } return jp; } public ButtonGroups() { add(makeBPanel(JButton.class, ids)); add(makeBPanel(JToggleButton.class, ids)); add(makeBPanel(JCheckBox.class, ids)); add(makeBPanel(JRadioButton.class, ids)); } public static void main(String args[]) { Show.inFrame(new ButtonGroups(), 500, 300); } } ///:~
The title for the border is taken
from the name of the class, stripping off all the path information. The
AbstractButton is initialized to a JButton that has the label
“Failed” so if you ignore the exception message, you’ll still
see the problem on screen. The
getConstructor( )
method produces a
Constructor object that
takes the array of arguments of the types in the
Class array passed to
getConstructor( ). Then all you do is call
newInstance( ),
passing it an array of Object containing your actual arguments – in
this case, just the String from the ids array.
This adds a little complexity to
what is a simple process. To get “exclusive or” behavior with
buttons, you create a button group and add each button for which you want that
behavior to the group. When you run the program, you’ll see that all the
buttons except JButton exhibit this “exclusive or”
behavior.
You can use an
Icon inside a
JLabel or anything that inherits from AbstractButton (including
JButton,
JCheckbox,
JradioButton, and the
different kinds of
JMenuItem). Using
Icons with JLabels is quite straightforward (you’ll see an
example later). The following example explores all the additional ways you can
use Icons with buttons and their descendants.
You can use any gif files
you want, but the ones used in this example are part of the book’s code
distribution, available at www.BruceEckel.com. To open a file and bring
in the image, simply create an
ImageIcon and hand it the
file name. From then on, you can use the resulting Icon in your
program.
//: Faces.java // Icon behavior in JButtons package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Faces extends JPanel { static Icon[] faces = { new ImageIcon("face0.gif"), new ImageIcon("face1.gif"), new ImageIcon("face2.gif"), new ImageIcon("face3.gif"), new ImageIcon("face4.gif"), }; JButton jb = new JButton("JButton", faces[3]), jb2 = new JButton("Disable"); boolean mad = false; public Faces() { jb.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(mad) { jb.setIcon(faces[3]); mad = false; } else { jb.setIcon(faces[0]); mad = true; } jb.setVerticalAlignment(JButton.TOP); jb.setHorizontalAlignment(JButton.LEFT); } }); jb.setRolloverEnabled(true); jb.setRolloverIcon(faces[1]); jb.setPressedIcon(faces[2]); jb.setDisabledIcon(faces[4]); jb.setToolTipText("Yow!"); add(jb); jb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(jb.isEnabled()) { jb.setEnabled(false); jb2.setText("Enable"); } else { jb.setEnabled(true); jb2.setText("Disable"); } } }); add(jb2); } public static void main(String args[]) { Show.inFrame(new Faces(), 300, 200); } } ///:~
An Icon can be used in many
constructors, but you can also use
setIcon( ) to add or
change an Icon. This example also shows how a JButton (or any
AbstractButton) can set the various different sorts of icons that appear
when things happen to that button: when it’s pressed, disabled, or
“rolled over” (the
mouse moves over it without clicking). You’ll see that this gives the
button a rather animated feel.
Menus are much improved and more
flexible in Swing – for example, you can use them just about anywhere,
including panels and applets. The syntax for using them is much the same as it
was in the old AWT, and this preserves the same problem present in the old AWT:
you must hard-code your menus and there isn’t any support for menus as
resources (which, among other things, would make them easier to change for other
languages). In addition, menu code gets long-winded and sometimes messy. The
following approach takes a step in the direction of solving this problem by
putting all the information about each menu into a two-dimensional
array of Object (that way
you can put anything you want into the array). This array is organized so that
the first row represents the menu name, and the remaining rows represent the
menu items and their characteristics. You’ll notice the rows of the array
do not have to be uniform from one to the next – as long as your code
knows where everything should be, each row can be completely
different.
//: Menus.java // A menu-building system; also demonstrates // icons in labels and menu items. package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Menus extends JPanel { static final Boolean bT = new Boolean(true), bF = new Boolean(false); // Dummy class to create type identifiers: static class MType { MType(int i) {} }; static final MType mi = new MType(1), // Normal menu item cb = new MType(2), // Checkbox menu item rb = new MType(3); // Radio button menu item JTextField t = new JTextField(10); JLabel l = new JLabel("Icon Selected", Faces.faces[0], JLabel.CENTER); ActionListener a1 = new ActionListener() { public void actionPerformed(ActionEvent e) { t.setText( ((JMenuItem)e.getSource()).getText()); } }; ActionListener a2 = new ActionListener() { public void actionPerformed(ActionEvent e) { JMenuItem mi = (JMenuItem)e.getSource(); l.setText(mi.getText()); l.setIcon(mi.getIcon()); } }; // Store menu data as "resources": public Object[][] fileMenu = { // Menu name and accelerator: { "File", new Character('F') }, // Name type accel listener enabled { "New", mi, new Character('N'), a1, bT }, { "Open", mi, new Character('O'), a1, bT }, { "Save", mi, new Character('S'), a1, bF }, { "Save As", mi, new Character('A'), a1, bF}, { null }, // Separator { "Exit", mi, new Character('x'), a1, bT }, }; public Object[][] editMenu = { // Menu name: { "Edit", new Character('E') }, // Name type accel listener enabled { "Cut", mi, new Character('t'), a1, bT }, { "Copy", mi, new Character('C'), a1, bT }, { "Paste", mi, new Character('P'), a1, bT }, { null }, // Separator { "Select All", mi,new Character('l'),a1,bT}, }; public Object[][] helpMenu = { // Menu name: { "Help", new Character('H') }, // Name type accel listener enabled { "Index", mi, new Character('I'), a1, bT }, { "Using help", mi,new Character('U'),a1,bT}, { null }, // Separator { "About", mi, new Character('t'), a1, bT }, }; public Object[][] optionMenu = { // Menu name: { "Options", new Character('O') }, // Name type accel listener enabled { "Option 1", cb, new Character('1'), a1,bT}, { "Option 2", cb, new Character('2'), a1,bT}, }; public Object[][] faceMenu = { // Menu name: { "Faces", new Character('a') }, // Optinal last element is icon { "Face 0", rb, new Character('0'), a2, bT, Faces.faces[0] }, { "Face 1", rb, new Character('1'), a2, bT, Faces.faces[1] }, { "Face 2", rb, new Character('2'), a2, bT, Faces.faces[2] }, { "Face 3", rb, new Character('3'), a2, bT, Faces.faces[3] }, { "Face 4", rb, new Character('4'), a2, bT, Faces.faces[4] }, }; public Object[] menuBar = { fileMenu, editMenu, faceMenu, optionMenu, helpMenu, }; static public JMenuBar createMenuBar(Object[] menuBarData) { JMenuBar menuBar = new JMenuBar(); for(int i = 0; i < menuBarData.length; i++) menuBar.add( createMenu((Object[][])menuBarData[i])); return menuBar; } static ButtonGroup bgroup; static public JMenu createMenu(Object[][] menuData) { JMenu menu = new JMenu(); menu.setText((String)menuData[0][0]); menu.setMnemonic( ((Character)menuData[0][1]).charValue()); // Create redundantly, in case there are // any radio buttons: bgroup = new ButtonGroup(); for(int i = 1; i < menuData.length; i++) { if(menuData[i][0] == null) menu.add(new JSeparator()); else menu.add(createMenuItem(menuData[i])); } return menu; } static public JMenuItem createMenuItem(Object[] data) { JMenuItem m = null; MType type = (MType)data[1]; if(type == mi) m = new JMenuItem(); else if(type == cb) m = new JCheckBoxMenuItem(); else if(type == rb) { m = new JRadioButtonMenuItem(); bgroup.add(m); } m.setText((String)data[0]); m.setMnemonic( ((Character)data[2]).charValue()); m.addActionListener( (ActionListener)data[3]); m.setEnabled( ((Boolean)data[4]).booleanValue()); if(data.length == 6) m.setIcon((Icon)data[5]); return m; } Menus() { setLayout(new BorderLayout()); add(createMenuBar(menuBar), BorderLayout.NORTH); JPanel p = new JPanel(); p.setLayout(new BorderLayout()); p.add(t, BorderLayout.NORTH); p.add(l, BorderLayout.CENTER); add(p, BorderLayout.CENTER); } public static void main(String args[]) { Show.inFrame(new Menus(), 300, 200); } } ///:~
The goal is to allow the programmer
to simply create tables to represent each menu, rather than typing lines of code
to build the menus. Each table produces one menu, and the first row in the table
contains the menu name and its keyboard accelerator. The remaining rows contain
the data for each menu item: the string to be placed on the menu item, what type
of menu item it is, its keyboard accelerator, the actionlistener that is fired
when this menu item is selected, and whether this menu item is enabled. If a row
starts with null it is treated as a separator.
To prevent wasteful and tedious
multiple creations of Boolean objects and type flags, these are created
as static final values at the beginning of the class: bT and
bF to represent Booleans and different objects of the dummy class
MType to describe normal menu items (mi), checkbox menu items
(cb), and radio button menu items (rb). Remember that an array of
Object may hold only Object handles and not primitive
values.
This example also shows how
JLabels and
JMenuItems (and their
descendants) may hold Icons. An Icon is placed into the
JLabel via its constructor and changed when the corresponding menu item
is selected.
The menuBar array contains
the handles to all the file menus in the order that you want them to appear on
the menu bar. You pass this array to createMenuBar( ), which breaks
it up into individual arrays of menu data, passing each to
createMenu( ). This method, in turn, takes the first line of the
menu data and creates a
JMenu from it, then calls
createMenuItem( ) for each of the remaining lines of menu data.
Finally, createMenuItem( ) parses each line of menu data and
determines the type of menu and its attributes, and creates that menu item
appropriately. In the end, as you can see in the Menus( )
constructor, to create a menu from these tables say
createMenuBar(menuBar) and everything is handled
recursively.
This example does not take care of
building cascading menus, but you should have enough of the concept that you can
add that capability if you need it.
The most straightforward way
to
implement a JPopupMenu is
to create an inner class that extends MouseAdapter, then add an object of
that inner class to each component which should produce popup
behavior:
//: Popup.java // Creating popup menus with Swing package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class Popup extends JPanel { JPopupMenu popup = new JPopupMenu(); JTextField t = new JTextField(10); public Popup() { add(t); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenuItem m = new JMenuItem("Hither"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Yon"); m.addActionListener(al); popup.add(m); m = new JMenuItem("Afar"); m.addActionListener(al); popup.add(m); popup.addSeparator(); m = new JMenuItem("Stay Here"); m.addActionListener(al); popup.add(m); PopupListener pl = new PopupListener(); addMouseListener(pl); t.addMouseListener(pl); } class PopupListener extends MouseAdapter { public void mousePressed(MouseEvent e) { maybeShowPopup(e); } public void mouseReleased(MouseEvent e) { maybeShowPopup(e); } private void maybeShowPopup(MouseEvent e) { if(e.isPopupTrigger()) { popup.show( e.getComponent(), e.getX(), e.getY()); } } } public static void main(String args[]) { Show.inFrame(new Popup(),200,150); } } ///:~
The same ActionListener is
added to each JMenuItem,
so that it fetches the text from the menu label and inserts it into the
JTextField.
List boxes and
combo boxes in Swing work much
as they do in the old AWT, but they also have increased functionality if you
need it. In addition, some conveniences have been added. For example, the
JList has a constructor
that takes an array of Strings to display (oddly enough this same feature
is not available in
JComboBox). Here’s
a simple example that shows the basic use of each:
//: ListCombo.java // List boxes & Combo boxes package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class ListCombo extends JPanel { public ListCombo() { setLayout(new GridLayout(2,1)); JList list = new JList(ButtonGroups.ids); add(new JScrollPane(list)); JComboBox combo = new JComboBox(); for(int i = 0; i < 100; i++) combo.addItem(Integer.toString(i)); add(combo); } public static void main(String args[]) { Show.inFrame(new ListCombo(),200,200); } } ///:~
Something else that seems a bit odd
at first is that JLists do not automatically provide scrolling, even
though that’s something you always expect. Adding support for scrolling
turns out to be quite easy, as shown above – you simply wrap the
JList in a
JScrollPane and all the
details are automatically managed for
you.
A
slider allows the user to input
data by moving a point back and forth, which is intuitive in some situations
(volume controls, for example). A
progress bar displays data in a
relative fashion from “full” to “empty” so the user gets
a perspective. My favorite example for these is to simply hook the slider to the
progress bar so when you move the slider the progress bar changes
accordingly:
//: Progress.java // Using progress bars and sliders package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.border.*; public class Progress extends JPanel { JProgressBar pb = new JProgressBar(); JSlider sb = new JSlider(JSlider.HORIZONTAL, 0, 100, 60); public Progress() { setLayout(new GridLayout(2,1)); add(pb); sb.setValue(0); sb.setPaintTicks(true); sb.setMajorTickSpacing(20); sb.setMinorTickSpacing(5); sb.setBorder(new TitledBorder("Slide Me")); pb.setModel(sb.getModel()); // Share model add(sb); } public static void main(String args[]) { Show.inFrame(new Progress(),200,150); } } ///:~
The
JProgressBar is fairly
straightforward, but the
JSlider has a lot of
options, such as the orientation and major and minor tick marks. Notice how
straightforward it is to add a titled
border.
add(new JTree( new Object[] {"this", "that", "other"}));
This displays a primitive
tree. The API for trees is vast,
however – certainly one of the largest in Swing. It appears that you can
do just about anything with trees, but more sophisticated tasks might require
quite a bit of research and experimentation.
Fortunately, there is a middle
ground provided in the library: the “default” tree components, which
generally do what you need. So most of the time you can use these components,
and only in special cases will you need to delve in and understand trees more
deeply.
The following example uses the
“default” tree components to display a tree in an applet. When you
press the button, a new subtree is added under the currently-selected node (if
no node is selected, the root node is used):
//: Trees.java // Simple Swing tree example. Trees can be made // vastly more complex than this. package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.tree.*; // Takes an array of Strings and makes the first // element a node and the rest leaves: class Branch { DefaultMutableTreeNode r; public Branch(String[] data) { r = new DefaultMutableTreeNode(data[0]); for(int i = 1; i < data.length; i++) r.add(new DefaultMutableTreeNode(data[i])); } public DefaultMutableTreeNode node() { return r; } } public class Trees extends JPanel { String[][] data = { { "Colors", "Red", "Blue", "Green" }, { "Flavors", "Tart", "Sweet", "Bland" }, { "Length", "Short", "Medium", "Long" }, { "Volume", "High", "Medium", "Low" }, { "Temperature", "High", "Medium", "Low" }, { "Intensity", "High", "Medium", "Low" }, }; static int i = 0; DefaultMutableTreeNode root, child, chosen; JTree tree; DefaultTreeModel model; public Trees() { setLayout(new BorderLayout()); root = new DefaultMutableTreeNode("root"); tree = new JTree(root); // Add it and make it take care of scrolling: add(new JScrollPane(tree), BorderLayout.CENTER); // Capture the tree's model: model =(DefaultTreeModel)tree.getModel(); JButton test = new JButton("Press me"); test.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(i < data.length) { child = new Branch(data[i++]).node(); // What's the last one you clicked? chosen = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent(); if(chosen == null) chosen = root; // The model will create the // appropriate event. In response, the // tree will update itself: model.insertNodeInto(child, chosen, 0); // This puts the new node on the // currently chosen node. } } }); // Change the button's colors: test.setBackground(Color.blue); test.setForeground(Color.white); JPanel p = new JPanel(); p.add(test); add(p, BorderLayout.SOUTH); } public static void main(String args[]) { Show.inFrame(new Trees(),200,500); } } ///:~
The first class, Branch, is
a tool to take an array of String and build a
DefaultMutableTreeNode
with the first String as the root and the rest of the Strings in
the array as leaves. Then node( ) can be called to produce the root
of this “branch.”
The Trees class contains a
two-dimensional array of Strings from which Branches can be made
and a static int i to count through this array. The
DefaultMutableTreeNode objects hold the nodes, but the physical
representation on screen is controlled by the JTree and its associated
model, the
DefaultTreeModel. Note
that when the JTree is added to the applet, it is wrapped in a
JScrollPane – this
is all it takes to provide automatic scrolling.
The JTree is controlled
through its model. When you make a change to the model, the model
generates an event that causes the
JTree to perform any
necessary updates to the visible representation of the tree. In
init( ), the model is captured by calling
getModel( ). When
the button is pressed, a new “branch” is created. Then the currently
selected component is found (or the root if nothing is selected) and the
model’s
insertNodeInto( )
method does all the work of changing the tree and causing it to be
updated.
Most of the time an example like
the one above will give you what you need in a tree. However, trees have the
power to do just about anything you can imagine – everywhere you see the
word “default” in the example above, you can substitute your own
class to get different behavior. But beware: almost all of these classes have a
large interface, so you could spend a lot of time struggling to understand the
intricacies of trees.
Like trees,
tables in Swing are vast and
powerful. They are primarily intended to be the popular “grid”
interface to databases via Java Database Connectivity (JDBC, discussed in
Chapter 15) and thus they have a tremendous amount of flexibility, which you pay
for in complexity. There’s easily enough here to be the basis of a
full-blown spreadsheet and could probably justify an entire book. However, it is
also possible to create a relatively simple JTable if you understand the
basics.
The JTable controls how the
data is displayed, but the TableModel controls the data itself. So to
create a JTable you’ll typically create a TableModel first.
You can fully implement the TableModel interface, but it’s usually
simpler to inherit from the helper class
AbstractTableModel:
//: Table.java // Simple demonstration of JTable package c13.swing; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.table.*; import javax.swing.event.*; // The TableModel controls all the data: class DataModel extends AbstractTableModel { Object[][] data = { {"one", "two", "three", "four"}, {"five", "six", "seven", "eight"}, {"nine", "ten", "eleven", "twelve"}, }; // Prints data when table changes: class TML implements TableModelListener { public void tableChanged(TableModelEvent e) { for(int i = 0; i < data.length; i++) { for(int j = 0; j < data[0].length; j++) System.out.print(data[i][j] + " "); System.out.println(); } } } DataModel() { addTableModelListener(new TML()); } public int getColumnCount() { return data[0].length; } public int getRowCount() { return data.length; } public Object getValueAt(int row, int col) { return data[row][col]; } public void setValueAt(Object val, int row, int col) { data[row][col] = val; // Indicate the change has happened: fireTableDataChanged(); } public boolean isCellEditable(int row, int col) { return true; } }; public class Table extends JPanel { public Table() { setLayout(new BorderLayout()); JTable table = new JTable(new DataModel()); JScrollPane scrollpane = JTable.createScrollPaneForTable(table); add(scrollpane, BorderLayout.CENTER); } public static void main(String args[]) { Show.inFrame(new Table(),200,200); } } ///:~
DataModel contains an array
of data, but you could also get the data from some other source such as a
database. The constructor adds a TableModelListener which prints the
array every time the table is changed. The rest of the methods follow the Beans
naming convention, and are used by JTable when it wants to present the
information in DataModel. AbstractTableModel provides default
methods for setValueAt( ) and isCellEditable( ) that
prevent changes to the data, so if you want to be able to edit the data, you
must override these methods.
Once you have a TableModel,
you only need to hand it to the JTable constructor. All the details of
displaying, editing and updating will be taken care of for you. Notice that this
example also puts the JTable in a JScrollPane, which requires a
special JTable method.
Earlier in this chapter you were
introduced to the positively medieval
CardLayout, and saw how
you had to manage all the switching of the ugly cards yourself. Someone actually
thought this was a good design. Fortunately, Swing remedies this by providing
JTabbedPane, which handles
all the tabs, the switching, and everything. The contrast between
CardLayout and JTabbedPane is breathtaking.
The following example is quite fun
because it takes advantage of the design of the previous examples. They are all
built as descendants of JPanel, so this example will place each one of
the previous examples in its own pane on a JTabbedPane. You’ll
notice that the use of RTTI makes the example quite small and
elegant:
//: Tabbed.java // Using tabbed panes package c13.swing; import java.awt.*; import javax.swing.*; import javax.swing.border.*; public class Tabbed extends JPanel { static Object[][] q = { { "Felix", Borders.class }, { "The Professor", Buttons.class }, { "Rock Bottom", ButtonGroups.class }, { "Theodore", Faces.class }, { "Simon", Menus.class }, { "Alvin", Popup.class }, { "Tom", ListCombo.class }, { "Jerry", Progress.class }, { "Bugs", Trees.class }, { "Daffy", Table.class }, }; static JPanel makePanel(Class c) { String title = c.getName(); title = title.substring( title.lastIndexOf('.') + 1); JPanel sp = null; try { sp = (JPanel)c.newInstance(); } catch(Exception e) { System.out.println(e); } sp.setBorder(new TitledBorder(title)); return sp; } public Tabbed() { setLayout(new BorderLayout()); JTabbedPane tabbed = new JTabbedPane(); for(int i = 0; i < q.length; i++) tabbed.addTab((String)q[i][0], makePanel((Class)q[i][1])); add(tabbed, BorderLayout.CENTER); tabbed.setSelectedIndex(q.length/2); } public static void main(String args[]) { Show.inFrame(new Tabbed(),460,350); } } ///:~
Again, you can see the theme of an
array used for configuration: the first element is the String to be
placed on the tab and the second is the JPanel class that will be
displayed inside of the corresponding pane. In the Tabbed( )
constructor, you can see the two important JTabbedPane methods that are
used: addTab( ) to
put a new pane in, and
setSelectedIndex( )
to choose the pane to start with. (One in the middle is chosen just to show that
you don’t have to start with the first pane.)
When you call addTab( )
you supply it with the String for the tab and any
Component (that is, an
AWT Component, not just a JComponent, which is derived from the
AWT Component). The Component will be displayed in the pane. Once
you do this, no further management is necessary – the JTabbedPane
takes care of everything else for you (as it should).
The makePanel( ) method
takes the Class object of the class you want to create and uses
newInstance( ) to
create one, casting it to a JPanel (of course, this assumes that any
class you want to add must inherit from JPanel, but that’s been the
structure used for the examples in this section). It adds a
TitledBorder that
contains the name of the class and returns the result as a JPanel to be
used in addTab( ).
When you run the program
you’ll see that the JTabbedPane automatically stacks the tabs if
there are too many of them to fit on one
row.
Windowing environments commonly
contain a standard set of
message boxes that allow you to
quickly post information to the user or to capture information from the user. In
Swing, these message boxes are contained in
JOptionPane. You have
many different possibilities (some quite sophisticated), but the ones
you’ll most commonly use are probably the message dialog and confirmation
dialog, invoked using the static
JOptionPane.showMessageDialog( ) and JOptionPane.
showConfirmDialog( ).
This section was meant only to give
you an introduction to the power of Swing and to get you started so you could
see how relatively simple it is to feel your way through the libraries. What
you’ve seen so far will probably suffice for a good portion of your UI
design needs. However, there’s a lot more to Swing – it’s
intended to be a fully-powered UI design tool kit. If you don’t see what
you need here, delve into the online documentation from Sun and search the Web.
There’s probably a way to accomplish just about everything you can
imagine.
Of all the libraries in Java, the
AWT has seen the most dramatic changes from Java 1.0 to
Java 1.2. The Java 1.0 AWT was roundly criticized as
being one of the worst designs seen, and while it would allow you to create
portable programs, the resulting GUI was “equally mediocre on all
platforms.” It was also limiting, awkward, and unpleasant to use compared
with native application development tools on a particular
platform.
When Java
1.1 introduced the new event model and Java Beans, the
stage was set – now it was possible to create GUI components that could be
easily dragged and dropped inside visual application builder tools. In addition,
the design of the event model and Beans clearly shows strong consideration for
ease of programming and maintainable code (something that was not evident in the
1.0 AWT). But it wasn’t until the GUI components – the JFC/Swing
classes – appeared that the job was finished. With the Swing components,
cross-platform GUI programming can be a civilized experience.
Actually, the only thing
that’s missing is the application builder tool, and this is where the real
revolution lies. Microsoft’s Visual Basic and
Visual C++ require their application builder tools, as does
Borland’s Delphi and C++ Builder. If you want the
application builder tool to get better, you have to cross your fingers and hope
the vendor will give you what you want. But Java is an open environment, and so
not only does it allow for competing application builder environments, it
encourages them. And for these tools to be taken seriously, they must support
Java Beans. This means a leveled playing field: if a better application builder
tool comes along, you’re not tied to the one you’ve been using
– you can pick up and move to the new one and increase your productivity.
This kind of competitive environment for GUI application builder tools has not
been seen before, and the resulting competition can generate only positive
results for the productivity of the
programmer.
[51]
It is assumed that the reader is familiar with the basics of HTML. It’s
not too hard to figure out, and there are lots of books and
resources.
[52]
Because the appletviewer ignores everything but APPLET tags, you can put those
tags in the Java source file as
comments:
// <applet
code=MyApplet.class width=200
height=100></applet>
This
way, you can run "appletviewer MyApplet.java" and you don’t need to
create tiny HTML files to run tests.
[53]
ShowStatus( ) is also a method of Applet, so you can call it
directly, without calling getAppletContext( ).
[54]
This behavior is apparently a bug and will be fixed in a later version of
Java.
[55]
There is no MouseMotionEvent even though it seems like there ought to be.
Clicking and motion is combined into MouseEvent, so this second
appearance of MouseEvent in the table is not an error.
[56]
It also solves the problem of “callbacks” without adding any awkward
“method pointer” feature to Java.
[57]
At the time this section was written, the Swing library had been pronounced
“frozen” by Sun, so this code should compile and run without
problems as long as you’ve downloaded and installed the Swing library.
(You should be able to compile one of Sun’s included demonstration
programs to test your installation.) If you do encounter difficulties, check
www.BruceEckel.com for updated code.