[[[ Note: please notice that only
the code files have been changed to be Swing-compliant, and the prose is
essentially the way it was before, and refers to the old programs—that is
to say, more work is going to be done here. So please concentrate on the code
listings, and if you see changes that should be made, please include the entire
file after you’ve made the improvements, using the correction page
attached to this HTML file or at www.BruceEckel.com ]]]
A fundamental design guideline is
“make simple things easy, and difficult things
possible[59].”
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. The Java 1.0 AWT programming model is also awkward and
non-object-oriented. A student in one of my seminars (who had been at Sun during
the creation of Java) explained why: the original AWT had been conceptualized,
designed and implemented in a month. Certainly a marvel of productivity, and
also an object lesson in why design is important.
Much of the situation improved with the
Java 1.1 AWT event model, which takes a much clearer,
object-oriented approach, along with the addition of Java Beans, a component
programming model that is oriented toward the easy creation of visual
programming environments. Java 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.
This chapter does not cover anything but
the modern, Java 2 Swing library, and makes the reasonable assumption that Swing
is the final destination GUI library for Java. If for some reason you need to
use the original “old” AWT (because you’re supporting old code
or old browsers), you can find that introduction in the online first edition of
this book at www.BruceEckel.com.
Early in this chapter, you’ll see
how things are different when you want to create an applet vs. a regular
application using Swing, 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. Almost all the GUI examples in this book will be runnable as either
applets or applications.
Please be aware that this is not a
comprehensive glossary of all the methods for the described classes. The Swing
library is vast, and the goal of this chapter is only to get you started with
the essentials and comfortable with the concepts. When you’re looking for
more sophistication, make sure you go to the HTML JDK library documents from
http://java.sun.com to look for the javax.swing classes and
methods—because of the simplicity of Swing, this will often be enough
information to solve your problem. There are numerous (rather thick) books
dedicated solely to Swing and you’ll want to go to those if you need to
modify the default Swing behavior.
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, applets are a powerful tool that supports 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 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. Swing can also be used to
build GUI interfaces for regular applications.
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 ArrayList 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[60].
Applets are built
using an application framework. You inherit from class JApplet and
override the appropriate methods. There are a few methods that control the
creation and execution of an applet on a Web page:
Method |
Operation |
---|---|
init( ) |
Called when the applet is first created
to perform first-time initialization of the applet, including component layout.
You’ll always override this method. |
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( ). |
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 |
With this information you can create a
simple applet:
//: c13:Applet1.java // Very simple applet. import javax.swing.*; import java.awt.*; public class Applet1 extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
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[61] to tell
the page how to load and run the applet.
This process used to be very simple, when
Java was simple and everyone was on the same bandwagon and incorporated the same
Java support inside their Web browsers. Then you might have been able to get
away with a very simple bit of HTML inside your Web page, like
this:
<applet code=Applet1 width=100 height=50> </applet>
Then along came the browser &
language wars, and we (programmers and end users alike) lost. After awhile,
JavaSoft realized that we could no longer expect browsers to support the correct
flavor of Java, and the only solution was to provide some kind of add-on that
would conform to a browser’s extension mechanism. By using the extension
mechanism (which a browser vendor cannot disable—in an attempt to gain
competitive advantage—without breaking all the 3rd party
extensions) JavaSoft guarantees that Java cannot be shut out of the Web browser
by an antagonistic vendor.
With Internet Explorer, the extension
mechanism is the ActiveX control, and with Netscape the extension mechanism is
the plug-in. In your html code, you must provide tags to support both.
Here’s what the simplest resulting HTML page looks like for
Applet1:
//:! c13:Applet1.html <html><head><title>Applet1</title></head><hr> <OBJECT classid="clsid:8AD9C840-044E-11D1-B3E9-00805F499D93" width="100" height="50" align="baseline" codebase="http://java.sun.com/products/plugin/1.2.2/jinstall-1_2_2-win.cab#Version=1,2,2,0"> <PARAM NAME="code" VALUE="Applet1.class"> <PARAM NAME="codebase" VALUE="."> <PARAM NAME="type" VALUE="application/x-java-applet;version=1.2.2"> <COMMENT> <EMBED type= "application/x-java-applet;version=1.2.2" width="200" height="200" align="baseline" code="Applet1.class" codebase="." pluginspage="http://java.sun.com/products/plugin/1.2/plugin-install.html"> <NOEMBED> </COMMENT> No Java 2 support for APPLET!! </NOEMBED> </EMBED> </OBJECT> <hr></body></html> ///:~
Some of these lines were too long and had
to be wrapped to fit on the page. The code in this book’s source code (on
the book’s CD ROM, and downloadable from www.BruceEckel.com) will
work without having to worry about correcting line wraps.
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">
Sun’s JDK (freely downloadable from
http://java.sun.com) 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. Because the appletviewer ignores everything but APPLET
tags, you can put those tags in the Java source file as
comments:
// <applet code=MyApplet 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. Also, you can see from looking at the example the HTML tag that must be
inserted into a Web page.
For example, you can add the commented
HTML tags to Applet1.java:
//: c13:Applet1b.java // Embedding the applet tag for Appletviewer. // <applet code=Applet1b width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; public class Applet1b extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } } ///:~
Now you can invoke the applet with the
command
Appletviewer Applet1b.java
For “plain” applets in this
book, this form will be used for easy testing (later, you’ll see another
coding approach which will allow you to execute applets from the command line
without the Appletviewer).
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. 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 (since the applet is just a file or set of files, the ISP
does not have to provide any special support for Java). 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 available for free or as
shareware. 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 on the client machine 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.
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.
Often
you’ll want to be able to create a class that can be invoked as either a
window or an applet. This is especially convenient when you’re testing the
applets, since it’s typically much faster and easier to run the resulting
applet-application from the command line than it is to start up a Web browser or
the Appletviewer.
To create an applet that can be run from
the console command line, 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 Applet1b.java modified to work as both an
application and an applet:
//: c13:Applet1c.java // An application and an applet. // <applet code=Applet1c width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Applet1c extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } // A main() for the application: public static void main(String[] args) { JApplet applet = new Applet1b(); JFrame frame = new JFrame("Applet1b"); // To close the application: Console.setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(100,50); applet.init(); applet.start(); frame.setVisible(true); } } ///:~
main( ) is the only element
added to the applet, and the rest of the applet is untouched. In fact, you can
usually copy and paste main( ) into your own applets with little
modification. 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:
frame.setVisible(true);
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
examples in the rest of this chapter:
//: com:bruceeckel:swing:Console.java // Tool for running Swing demos from the // console, both applets and JFrames. package com.bruceeckel.swing; import javax.swing.*; import java.awt.event.*; public class Console { // Create a title string from the class name: public static String title(Object o) { String t = o.getClass().toString(); // Remove the word "class": if(t.indexOf("class") != -1) t = t.substring(6); return t; } public static void setupClosing(JFrame frame) { // The JDK 1.2 Solution as an // anonymous inner class: frame.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { System.exit(0); } }); // The improved solution in JDK 1.3: // frame.setDefaultCloseOperation( // EXIT_ON_CLOSE); } public static void run(JFrame frame, int width, int height) { setupClosing(frame); frame.setSize(width, height); frame.setVisible(true); } public static void run(JApplet applet, int width, int height) { JFrame frame = new JFrame(title(applet)); setupClosing(frame); frame.getContentPane().add(applet); frame.setSize(width, height); applet.init(); applet.start(); frame.setVisible(true); } public static void run(JPanel panel, int width, int height) { JFrame frame = new JFrame(title(panel)); setupClosing(frame); frame.getContentPane().add(panel); frame.setSize(width, height); frame.setVisible(true); } } ///:~
The common base class between
JApplet and JPanel is Container. Only if it’s a
JApplet is init( ) and start( ) called.
The method setupClosing( ) adds a
window listener which is a static object that is responsible for closing
the window.
You’ll see on the lines below this
a more succinct form to close the window:
//#frame.setDefaultCloseOperation(EXIT_ON_CLOSE);
This works with JDK 1.3, but is commented
out here for backward compatibility with JDK 1.2.
Now any applet can be run from the
command line by creating a main( ) containing the a line like
this:
Console.run(new MyClass(), 500, 300);
in which the last two arguments are the
display width and height. Here’s Applet1c.java modified to use
Console:
//: c13:Applet1d.java // Console runs applets from the command line. // <applet code=Applet1d width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Applet1d extends JApplet { public void init() { getContentPane().add(new JLabel("Applet!")); } public static void main(String[] args) { Console.run(new Applet1c(), 100, 50); } } ///:~
This allows the elimination of repeated
code while providing the greatest flexibility in running the
examples.
If you’re using Windows, you can
simplify the process of running a command-line Java program by configuring the
Windows Explorer (this is the file browser in Windows, not the Internet
Explorer) so that you can simply double-click on a .class file to execute
it. There are several steps in this process.
First, download and install the Perl
programming language from www.Perl.org. You’ll find the
instructions on that site.
Next, create the following script (it is
part of this book’s source-code package):
//:! c13:RunJava.bat @rem = '--*-Perl-*-- @echo off perl -x -S "%0" %1 %2 %3 %4 %5 %6 %7 %8 %9 goto endofperl @rem '; #!perl $file = $ARGV[0]; $file =~ s/(.*)\..*/\1/; $file =~ s/(.*\\)*(.*)/$+/; ´java $file´; __END__ :endofperl ///:~
Now, open the Windows Explorer, select
“View,” “Folder Options,” then click on the “File
Types” tab. Press the “New Type” button. For
“Description of Type” enter “Java class file.” Under
“Actions” press the “New” button. For
“Action” enter “Open,” and for “Application used
to perform action” enter a line like this:
"c:\aaa\Perl\RunJava.bat" "%L"
Making a button is quite simple: you just
call the JButton
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 reference for the button so you can refer
to it later.
The JButton 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( ):
//: c13:Button1.java // Putting buttons on an applet. // <applet code=Button1 width=200 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button1 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button1(), 200, 50); } } ///:~
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
this 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.
Swing 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:
//: c13:Button2.java // Capturing button presses. // <applet code=Button2 width=200 height=50> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button2 extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); class BL implements ActionListener { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); getAppletContext().showStatus(name); } } BL al = new BL(); public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button2(), 200, 50); } } ///:~
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 reference 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.[62] You can
print 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.
It is often more convenient to code the
ActionListener as an anonymous inner class, especially since you tend to
only use a single instance of each listener class. Button2.java can be
modified to use an anonymous inner class as follows:
//: c13:Button2b.java // Using anonymous inner classes. // <applet code=Button2b width=200 height=50> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Button2b extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); getAppletContext().showStatus(name); } }; public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); } public static void main(String[] args) { Console.run(new Button2b(), 200, 50); } } ///:~
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:
//: c13:TextField1.java // Using the JTextField control. // <applet code=TextField1 width=350 height=75> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class TextField1 extends JApplet { JButton b1 = new JButton("Get Text"), b2 = new JButton("Set Text"); JTextField t = new JTextField("Starting text: ", 30); String s = new String(); ActionListener a1 = new ActionListener() { public void actionPerformed(ActionEvent e){ getAppletContext().showStatus(t.getText()); s = t.getSelectedText(); if(s == null) s = t.getText(); t.setEditable(true); } }; ActionListener a2 = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText("Inserted by Button 2: " + s); t.setEditable(false); } }; public void init() { b1.addActionListener(a1); b2.addActionListener(a2); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t); } public static void main(String[] args) { Console.run(new TextField1(), 350, 75); } } ///:~
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.
//: c13:TextArea1.java // Using the JTextArea control. // <applet code=TextArea1 width=350 height=200> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class TextArea1 extends JApplet { JButton b1 = new JButton("Text Area 1"), b2 = new JButton("Text Area 2"), b3 = new JButton("Replace Text"), b4 = new JButton("Insert Text"); JTextArea t1 = new JTextArea("t1", 1, 30), t2 = new JTextArea("t2", 4, 30); ActionListener a1 = new ActionListener() { public void actionPerformed(ActionEvent e){ getAppletContext().showStatus(t1.getText()); } }; ActionListener a2 = new ActionListener() { public void actionPerformed(ActionEvent e){ t2.setText("Inserted by Button 2"); t2.append(": " + t1.getText()); getAppletContext().showStatus(t2.getText()); } }; ActionListener a3 = new ActionListener() { public void actionPerformed(ActionEvent e){ String s = " Replacement "; t2.replaceRange(s, 3, 3 + s.length()); } }; ActionListener a4 = new ActionListener() { public void actionPerformed(ActionEvent e){ t2.insert(" Inserted ", 10); } }; public void init() { b1.addActionListener(a1); b2.addActionListener(a2); b3.addActionListener(a3); b4.addActionListener(a4); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(t1); cp.add(b2); cp.add(t2); cp.add(b3); cp.add(b4); } public static void main(String[] args) { Console.run(new TextArea1(), 350, 200); } } ///:~
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
JLabel 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 JLabel 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 or a label with initial text in it (which is what you’ll
typically do). Less frequently used options include changing the alignment of
labels. You can also change the label with
setText( ) and
and read the value with
getText( ). This
example shows things you might commonly do with labels:
//: c13:Label1.java // Using JLabels. // <applet code=Label1 width=200 height=100> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Label1 extends JApplet { JTextField t1 = new JTextField("t1", 10); JLabel labl1 = new JLabel("TextField t1"), labl2 = new JLabel(" "); JButton b1 = new JButton("Test 1"), b2 = new JButton("Test 2"); ActionListener a1 = new ActionListener() { public void actionPerformed(ActionEvent e) { t1.setText("Button 1"); labl2.setText("Text set into Label"); } }; ActionListener a2 = new ActionListener() { public void actionPerformed(ActionEvent e) { t1.setText("Button 2"); labl1.setText("Hello"); } }; public void init() { b1.addActionListener(a1); b2.addActionListener(a2); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(labl1); cp.add(t1); cp.add(b1); cp.add(labl2); cp.add(b2); } public static void main(String[] args) { Console.run(new Label1(), 200, 100); } } ///:~
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.
Any component that can take text can also
take HTML text, which it will reformat according to HTML rules. This means you
can very easily add fancy text to a Swing component. For
example,
JButton bigButton = new JButton( "<html><b><font size=+2>" + "Hello!<br><i>Press me now!" ));
This produces [[ etc. ]]
[[rewrite]] The same trick works for
JLabel, JTabbedPane, JMenuItem, and JToolTip. Future
releases of Swing may also add HTML support to JRadioButton and
JCheckBox
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:
//: c13:CheckBox1.java // Using JCheckBoxes. // <applet code=CheckBox1 width=200 height=200> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class CheckBox1 extends JApplet { JTextArea t = new JTextArea(6, 15); JCheckBox cb1 = new JCheckBox("Check Box 1"), cb2 = new JCheckBox("Check Box 2"), cb3 = new JCheckBox("Check Box 3"); public void init() { cb1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("1", cb1); } }); cb2.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("2", cb2); } }); cb3.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ trace("3", cb3); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(cb1); cp.add(cb2); cp.add(cb3); } void trace(String b, JCheckBox cb) { if(cb.isSelected()) t.append("Box " + b + " Set\n"); else t.append("Box " + b + " Cleared\n"); } public static void main(String[] args) { Console.run(new CheckBox1(), 200, 200); } } ///:~
Notice that in this example, the
anonymous inner class that implements ActionListener is created inline,
so no intermediate variable is used.
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.
Swing 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:
//: c13:RadioButton1.java // Using JRadioButtons. // <applet code=RadioButton1 // width=200 height=75> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class RadioButton1 extends JApplet { JTextField t = new JTextField("Radio button 2", 15); ButtonGroup g = new ButtonGroup(); JRadioButton rb1 = new JRadioButton("one", false), rb2 = new JRadioButton("two", true), rb3 = new JRadioButton("three",false); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { t.setText("Radio button " + ((JRadioButton)e.getSource()).getText()); } }; public void init() { rb1.addActionListener(al); rb2.addActionListener(al); rb3.addActionListener(al); g.add(rb1); g.add(rb2); g.add(rb3); t.setEditable(false); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(rb1); cp.add(rb2); cp.add(rb3); } public static void main(String[] args) { Console.run(new RadioButton1(), 200, 75); } } ///:~
To display the state, a 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
two” 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:
//: c13:Choice1.java // Using drop-down lists. // <applet code=Choice1 // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class Choice1 extends JApplet { String[] description = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant", "Somnescent", "Timorous", "Florid", "Putrescent" }; JTextField t = new JTextField(15); JComboBox c = new JComboBox(); JButton b = new JButton("Add items"); int count = 0; public void init() { for(int i = 0; i < 4; i++) c.addItem(description[count++]); t.setEditable(false); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ if(count < description.length) c.addItem(description[count++]); } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText("index: "+ c.getSelectedIndex() + " " + ((JComboBox)e.getSource()) .getSelectedItem()); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(c); cp.add(b); } public static void main(String[] args) { Console.run(new Choice1(), 200, 100); } } ///:~
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[63] (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:
//: c13:List1.java // Using JLists. // <applet code=List1 // width=200 height=350> </applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class List1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JList list = new JList(flavors); JTextArea t = new JTextArea(flavors.length + 1, 15); public void init() { t.setEditable(false); list.addListSelectionListener( new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { t.setText(""); // Erase the text area Object[] items= list.getSelectedValues(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); cp.add(list); } public static void main(String[] args) { Console.run(new List1(), 200, 350); } } ///:~
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.
The
JTabbedPane allows you to
create a “tabbed dialog,” which has file-folder tabs running across
one edge, and all you have to do is press a tab to bring forward a different
dialog.
//: c13:TabbedPane1.java // Demonstrates the Tabbed Pane. // <applet code=TabbedPane1 // width=350 height=200> </applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class TabbedPane1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTabbedPane tabs = new JTabbedPane(); public void init() { for(int i = 0; i < flavors.length; i++) tabs.addTab(flavors[i], new JButton("Tabbed pane " + i)); tabs.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { getAppletContext().showStatus( "Tab selected: " + tabs.getSelectedIndex()); } }); getContentPane().add(tabs); } public static void main(String[] args) { Console.run(new TabbedPane1(), 350, 200); } } ///:~
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.
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.
You can see this by resizing the window when you run the program from the
console command line.
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( ).
//: c13:MessageBoxes.java // Demonstrates JoptionPane. // <applet code=MessageBoxes // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class MessageBoxes extends JApplet { JButton[] b = { new JButton("alert"), new JButton("yes/no"), new JButton("Color"), new JButton("input"), new JButton("3 vals") }; ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String id = ((JButton)e.getSource()).getText(); if(id.equals("alert")) JOptionPane.showMessageDialog(null, "There's a bug on you!", "Hey!", JOptionPane.ERROR_MESSAGE); else if(id.equals("yes/no")) JOptionPane.showConfirmDialog(null, "or no", "choose yes", JOptionPane.YES_NO_OPTION); else if(id.equals("Color")) { Object[] options = { "Red", "Green" }; int sel = JOptionPane.showOptionDialog( null, "Choose a Color!", "Warning", JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); if(sel != JOptionPane.CLOSED_OPTION) getAppletContext().showStatus( "Color Selected: " + options[sel]); } else if(id.equals("input")) { String val = JOptionPane.showInputDialog( "How many fingers do you see?"); getAppletContext().showStatus(val); } else if(id.equals("3 vals")) { Object[] selections = { "First", "Second", "Third" }; Object val = JOptionPane.showInputDialog( null, "Choose one", "Input", JOptionPane.INFORMATION_MESSAGE, null, selections, selections[0]); if(val != null) getAppletContext().showStatus( val.toString()); } } }; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < b.length; i++) { b[i].addActionListener(al); cp.add(b[i]); } } public static void main(String[] args) { Console.run(new MessageBoxes(), 200, 100); } } ///:~
//: c13:SimpleMenus.java // <applet code=SimpleMenus // width=200 height=75> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class SimpleMenus extends JApplet { JTextField t = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText( ((JMenuItem)e.getSource()).getText()); } }; JMenu[] menus = { new JMenu("Winken"), new JMenu("Blinken"), new JMenu("Nod") }; JMenuItem[] items = { new JMenuItem("Fee"), new JMenuItem("Fi"), new JMenuItem("Fo"), new JMenuItem("Zip"), new JMenuItem("Zap"), new JMenuItem("Zot"), new JMenuItem("Olly"), new JMenuItem("Oxen"), new JMenuItem("Free") }; public void init() { JMenuBar mb = new JMenuBar(); setJMenuBar(mb); for(int i = 0; i < items.length; i++) { items[i].addActionListener(al); menus[i%3].add(items[i]); } for(int i = 0; i < menus.length; i++) mb.add(menus[i]); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); } public static void main(String[] args) { Console.run(new SimpleMenus(), 200, 75); } } ///:~
//: c13:DialogDemo.java // Creating and using Dialog Boxes. // <applet code=DialogDemo width=125 height=50> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; class MyDialog extends JDialog { public MyDialog(JFrame parent) { super(parent, "My dialog", true); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JLabel("Here is my dialog")); JButton ok = new JButton("OK"); ok.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ setVisible(false); // Closes the dialog } }); cp.add(ok); setSize(150,125); } } public class DialogDemo extends JApplet { JButton b1 = new JButton("Dialog Box"); MyDialog dlg = new MyDialog(null); public void init() { b1.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ dlg.show(); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); } public static void main(String[] args) { Console.run(new DialogDemo(), 125, 50); } } ///:~
If you’ve been running these
programs, you’ll note that anything that pops up out of an applet,
including dialog boxes, is “untrusted.” That is, you get a warning
in the window that’s been popped up. 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. 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 is not so common to see an applet that uses a
dialog box.
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.
//: c13:FlowLayout1.java // Demonstrates FlowLayout. // <applet code=FlowLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class FlowLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new FlowLayout1(), 300, 250); } } ///:~
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:
//: c13:BorderLayout1.java // Demonstrates BorderLayout. // <applet code=BorderLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class BorderLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new BorderLayout()); cp.add(BorderLayout.NORTH, new JButton("North")); cp.add(BorderLayout.SOUTH, new JButton("South")); cp.add(BorderLayout.EAST, new JButton("East")); cp.add(BorderLayout.WEST, new JButton("West")); cp.add(BorderLayout.CENTER, new JButton("Center")); } public static void main(String[] args) { Console.run(new BorderLayout1(), 300, 250); } } ///:~
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.
//: c13:GridLayout1.java // Demonstrates GridLayout. // <applet code=GridLayout1 // width=300 height=250> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class GridLayout1 extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(7,3)); for(int i = 0; i < 20; i++) cp.add(new JButton("Button " + i)); } public static void main(String[] args) { Console.run(new GridLayout1(), 300, 250); } } ///:~
In this case there are 21 slots but only
20 buttons. The last slot is left empty; no “balancing” goes on with
a
GridLayout.
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 2 by Horstmann & Cornell
(Prentice-Hall, 1999) to get you
started.
Because so many people had so much
trouble understanding and working with GridBagLayout, Javasoft introduced
BoxLayout in Java 2, which gives you many of the benefits of
GridBagLayout without the grief. BoxLayout allows you to control
the placement of components either vertically or horizontally, and control the
space between the components using something call “Struts and Glue.”
First, let’s look at using the BoxLayout directly, as all the other
layout managers are used:
//: c13:BoxLayout1.java // Vertical and horizontal BoxLayouts. // <applet code=BoxLayout1 // width=450 height=200> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class BoxLayout1 extends JApplet { public void init() { JPanel jpv = new JPanel(); jpv.setLayout( new BoxLayout(jpv, BoxLayout.Y_AXIS)); for(int i = 0; i < 5; i++) jpv.add(new JButton("" + i)); JPanel jph = new JPanel(); jph.setLayout( new BoxLayout(jph, BoxLayout.X_AXIS)); for(int i = 0; i < 5; i++) jph.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, jpv); cp.add(BorderLayout.SOUTH, jph); } public static void main(String[] args) { Console.run(new BoxLayout1(), 450, 200); } } ///:~
The constructor for BoxLayout is a
bit different than the other layout managers—you provide the
Container that is to be controlled by the BoxLayout as the first
argument, and the direction of the layout as the second
argument.
To simplify matters, there’s a
special container called Box that uses BoxLayout as its native
manager. The following example lays out components horizontally and vertically
using Box, which has two static methods to create boxes with vertical and
horizontal alignment:
//: c13:Box1.java // Vertical and horizontal BoxLayouts. // <applet code=Box1 // width=450 height=200> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box1 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) bv.add(new JButton("" + i)); Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) bh.add(new JButton("" + i)); Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box1(), 450, 200); } } ///:~
//: c13:Box2.java // Adding struts. // <applet code=Box2 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box2 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); for(int i = 0; i < 5; i++) { bv.add(new JButton("" + i)); bv.add(Box.createVerticalStrut(i*10)); } Box bh = Box.createHorizontalBox(); for(int i = 0; i < 5; i++) { bh.add(new JButton("" + i)); bh.add(Box.createHorizontalStrut(i*10)); } Container cp = getContentPane(); cp.add(BorderLayout.EAST, bv); cp.add(BorderLayout.SOUTH, bh); } public static void main(String[] args) { Console.run(new Box2(), 450, 300); } } ///:~
//: c13:Box3.java // Using Glue. // <applet code=Box3 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box3 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JLabel("Hello")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("Applet")); bv.add(Box.createVerticalGlue()); bv.add(new JLabel("World")); Box bh = Box.createHorizontalBox(); bh.add(new JLabel("Hello")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("Applet")); bh.add(Box.createHorizontalGlue()); bh.add(new JLabel("World")); bv.add(Box.createVerticalGlue()); bv.add(bh); bv.add(Box.createVerticalGlue()); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box3(), 450, 300); } } ///:~
//: c13:Box4.java // Rigid Areas are like pairs of struts. // <applet code=Box4 // width=450 height=300> </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Box4 extends JApplet { public void init() { Box bv = Box.createVerticalBox(); bv.add(new JButton("Top")); bv.add(Box.createRigidArea( new Dimension(120, 90))); bv.add(new JButton("Bottom")); Box bh = Box.createHorizontalBox(); bh.add(new JButton("Left")); bh.add(Box.createRigidArea( new Dimension(160, 80))); bh.add(new JButton("Right")); bv.add(bh); getContentPane().add(bv); } public static void main(String[] args) { Console.run(new Box4(), 450, 300); } } ///:~
You should be aware that rigid areas are
a bit controversial. Since they use absolute values, some people feel that they
cause more trouble than they are
worth.
It is possible to set the absolute
position of the graphical components in your Frame or Applet in this
way:
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 a TrackEvent “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
TrackEvent. 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.
//: c13:TrackEvent.java // Show events as they happen. // <applet code=TrackEvent // width=700 height=500></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; class MyButton extends JButton { HashMap h; public MyButton(TrackEvent parent, Color color, String label) { super(label); h = parent.h; setBackground(color); addFocusListener(fl); addKeyListener(kl); addMouseListener(ml); addMouseMotionListener(mml); } void report(String field, String msg) { ((JTextField)h.get(field)).setText(msg); } FocusListener fl = new FocusListener() { public void focusGained(FocusEvent e) { report("focusGained", e.paramString()); } public void focusLost(FocusEvent e) { report("focusLost", e.paramString()); } }; KeyListener kl = new KeyListener() { public void keyPressed(KeyEvent e) { report("keyPressed", e.paramString()); } public void keyReleased(KeyEvent e) { report("keyReleased", e.paramString()); } public void keyTyped(KeyEvent e) { report("keyTyped", e.paramString()); } }; MouseListener ml = new MouseListener() { public void mouseClicked(MouseEvent e) { report("mouseClicked", e.paramString()); } public void mouseEntered(MouseEvent e) { report("mouseEntered", e.paramString()); } public void mouseExited(MouseEvent e) { report("mouseExited", e.paramString()); } public void mousePressed(MouseEvent e) { report("mousePressed", e.paramString()); } public void mouseReleased(MouseEvent e) { report("mouseReleased", e.paramString()); } }; MouseMotionListener mml = new MouseMotionListener() { public void mouseDragged(MouseEvent e) { report("mouseDragged", e.paramString()); } public void mouseMoved(MouseEvent e) { report("mouseMoved", e.paramString()); } }; } public class TrackEvent extends JApplet { HashMap h = new HashMap(); String[] event = { "focusGained", "focusLost", "keyPressed", "keyReleased", "keyTyped", "mouseClicked", "mouseEntered", "mouseExited","mousePressed", "mouseReleased", "mouseDragged", "mouseMoved" }; MyButton b1 = new MyButton(this, Color.blue, "test1"), b2 = new MyButton(this, Color.red, "test2"); public void init() { Container c = getContentPane(); c.setLayout(new GridLayout(event.length+1,2)); for(int i = 0; i < event.length; i++) { JTextField t = new JTextField(); t.setEditable(false); c.add(new JLabel(event[i], JLabel.RIGHT)); c.add(t); h.put(event[i], t); } c.add(b1); c.add(b2); } public static void main(String[] args) { Console.run(new TrackEvent(), 700, 500); } } ///:~
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 TrackEvent class. This contains a
HashMap 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
HashMap, 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 TrackEvent, 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 reference to reach
back to the parent window. Since that parent is an TrackEvent it contains
the HashMap h, and the get( ) method, when provided with the
appropriate String, will produce a reference 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.
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:
[Probably remove this, or move the
example]
Here’s an example of a windowed
application:
//: c13:ButtonApp.java // Creating an application. // <applet code=ButtonApp // width=200 height=200></applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class ButtonApp extends JApplet { JButton b1 = new JButton("Hello"), b2 = new JButton("Howdy"); JTextField t = new JTextField(15); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); t.setText(name); } }; public void init() { b1.addActionListener(al); b2.addActionListener(al); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t); } public static void main(String[] args) { Console.run(new ButtonApp(), 200, 200); } } ///:~
There are four different types inherited
from the abstract class
MenuComponent:
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 Swing you must hand assemble all the menus in source code. Here are the
ice cream flavors again, used to create menus:
//: c13:Menu1.java // Shows submenus, checkbox menu // items, and swapping menus // <applet code=Menu1 // width=300 height=100> </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Menu1 extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTextField t = new JTextField("No flavor", 30); JMenuBar mb1 = new JMenuBar(); JMenu f = new JMenu("File"), m = new JMenu("Flavors"), s = new JMenu("Safety"); // Alternative approach: JCheckBoxMenuItem[] safety = { new JCheckBoxMenuItem("Guard"), new JCheckBoxMenuItem("Hide") }; JMenuItem[] file = { new JMenuItem("Open"), new JMenuItem("Exit") }; // A second menu bar to swap to: JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { new JMenuItem("Foo"), new JMenuItem("Bar"), new JMenuItem("Baz"), }; JButton b = new JButton("Swap Menus"); public void init() { for(int i = 0; i < flavors.length; i++) { JMenuItem mi = new JMenuItem(flavors[i]); mi.addActionListener(al); m.add(mi); // Add separators at intervals: if((i+1) % 3 == 0) m.addSeparator(); } for(int i = 0; i < safety.length; i++) { safety[i].addActionListener(al); s.add(safety[i]); } f.add(s); for(int i = 0; i < file.length; i++) { file[i].addActionListener(al); f.add(file[i]); } mb1.add(f); mb1.add(m); t.setEditable(false); Container cp = getContentPane(); cp.add(t, BorderLayout.CENTER); // Set up the system for swapping menus: b.addActionListener(al); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) { other[i].addActionListener(al); fooBar.add(other[i]); } mb2.add(fooBar); setJMenuBar(mb1); } ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e) { String arg = e.getActionCommand(); Object source = e.getSource(); if(source.equals(b)) { JMenuBar m = getJMenuBar(); setJMenuBar(m == mb1 ? mb2 : mb1); validate(); // Refresh the frame } else if(source instanceof JMenuItem) { 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(source.equals(file[1])) System.exit(0); // CheckboxMenuItems cannot use String // matching; you must match getSource(): else if(source.equals(safety[0])) t.setText("Guard the Ice Cream! " + "Guarding is "+safety[0].getState()); else if(source.equals(safety[1])) t.setText("Hide the Ice Cream! " + "Is it cold? "+safety[1].getState()); else t.setText(arg); } } }; public static void main(String[] args) { Console.run(new Menu1(), 300, 100); } } ///:~
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 references 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 reference. 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.
//: c13:TicTacToe.java // Demonstration of dialog boxes // and creating your own components. // <applet code=TicTacToe // width=200 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; class ToeButton extends JPanel { int state = ToeDialog.BLANK; ToeDialog p; public ToeButton(ToeDialog parent) { p = parent; addMouseListener(ml); } MouseListener ml = new MouseAdapter() { public void mousePressed(MouseEvent e) { if(state == ToeDialog.BLANK) { state = p.turn; p.turn = (p.turn == ToeDialog.XX ? ToeDialog.OO : ToeDialog.XX); } else state = (state == ToeDialog.XX ? ToeDialog.OO : ToeDialog.XX); repaint(); } }; public void paintComponent(Graphics g) { super.paintComponent(g); int x1 = 0; int y1 = 0; int x2 = getWidth() - 1; int y2 = getHeight() - 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); } } } class ToeDialog extends JDialog { static final int BLANK = 0, XX = 1, OO = 2; int turn = XX; // Start with x's turn // w = number of cells wide // h = number of cells high public ToeDialog(int w, int h) { setTitle("The game itself"); // JDK 1.3 close dialog: //#setDefaultCloseOperation(DISPOSE_ON_CLOSE); // JDK 1.2 close dialog: addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ dispose(); } }); Container cp = getContentPane(); cp.setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) cp.add(new ToeButton(this)); setSize(w * 50, h * 50); } } public class TicTacToe extends JApplet { JTextField rows = new JTextField("3"), cols = new JTextField("3"); JButton go = new JButton("go"); public void init() { JPanel p = new JPanel(); p.setLayout(new GridLayout(2,2)); p.add(new JLabel("Rows", JLabel.CENTER)); p.add(rows); p.add(new JLabel("Columns", JLabel.CENTER)); p.add(cols); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); cp.add(go, BorderLayout.SOUTH); go.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ JDialog d = new ToeDialog( Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.setVisible(true); } }); } public static void main(String[] args) { Console.run(new TicTacToe(), 200, 100); } } ///:~
The ToeButton class keeps a
reference 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.
TicTacToe 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 reference 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 reference 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
JFileChooser encapsulates
these for easy use.
The following application exercises two
forms of JFileChooser dialogs, one for opening and one for saving. Most
of the code should by now be familiar, and all the interesting activities happen
in the actionlisteners for the two different button clicks:
//: c13:FileChooserTest.java // Demonstration of File dialog boxes. import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class FileChooserTest extends JFrame { JTextField filename = new JTextField(), dir = new JTextField(); JButton open = new JButton("Open"), save = new JButton("Save"); public FileChooserTest() { setTitle("File Dialog Test"); JPanel p = new JPanel(); open.addActionListener(new OpenL()); p.add(open); save.addActionListener(new SaveL()); p.add(save); Container cp = getContentPane(); cp.add(p, BorderLayout.SOUTH); dir.setEditable(false); filename.setEditable(false); p = new JPanel(); p.setLayout(new GridLayout(2,1)); p.add(filename); p.add(dir); cp.add(p, BorderLayout.NORTH); } class OpenL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // Demonstrate "Open" dialog: int rVal = c.showOpenDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } class SaveL implements ActionListener { public void actionPerformed(ActionEvent e) { JFileChooser c = new JFileChooser(); // Demonstrate "Save" dialog: int rVal = c.showSaveDialog(FileChooserTest.this); if(rVal == JFileChooser.APPROVE_OPTION) { filename.setText( c.getSelectedFile().getName()); dir.setText( c.getCurrentDirectory().toString()); } if(rVal == JFileChooser.CANCEL_OPTION) { filename.setText("You pressed cancel"); dir.setText(""); } } } public static void main(String[] args) { Console.run(new FileChooserTest(), 250, 110); } } ///:~
Note that there are many variations you
can apply to JFileChooser, including filters to narrow the file names
that you will allow.
For an “open file” dialog,
you use the constructor that takes two arguments; the first is the parent window
reference 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 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 reference 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.
//: c13:Button2New.java // Capturing button presses. // <applet code=Button2New // width=200 height=50> </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; // Must add this import com.bruceeckel.swing.*; public class Button2New extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { b1.addActionListener(new B1()); b2.addActionListener(new B2()); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.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"); } } public static void main(String[] args) { Console.run(new Button2New(), 200, 50); } } ///:~
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 Swing 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[64]
(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.
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.
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( ):
//: c13:Applet3.java // Shows init(), start() and stop() activities. // <applet code=Applet3 width=250 height=50> // </applet> import javax.swing.*; import java.awt.*; import com.bruceeckel.swing.*; public class Applet3 extends JApplet { 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); } public static void main(String[] args) { Console.run(new Applet3 (), 250, 50); } } ///:~
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.
This is similar to
TextField1.java, but it adds significant extra behavior:
//: c13:TextNew.java // Text fields and Java events. // <applet code=TextNew width=375 // height=125></applet> import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class TextNew extends JApplet { JButton b1 = new JButton("Get Text"), b2 = new JButton("Set Text"); JTextField t1 = new JTextField(30), t2 = new JTextField(30), t3 = new JTextField(30); String s = new String(); UpperCaseDocument ucd = new UpperCaseDocument(); public void init() { t1.setDocument(ucd); ucd.addDocumentListener(new T1()); b1.addActionListener(new B1()); b2.addActionListener(new B2()); DocumentListener dl = new T1(); t1.addActionListener(new T1A()); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b1); cp.add(b2); cp.add(t1); cp.add(t2); cp.add(t3); } class T1 implements DocumentListener { public void changedUpdate(DocumentEvent e){} public void insertUpdate(DocumentEvent e){ t2.setText(t1.getText()); t3.setText("Text: "+ t1.getText()); } public void removeUpdate(DocumentEvent 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 B1 implements ActionListener { public void actionPerformed(ActionEvent e) { if(t1.getSelectedText() == null) s = t1.getText(); else s = t1.getSelectedText(); t1.setEditable(true); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { ucd.setUpperCase(false); t1.setText("Inserted by Button 2: " + s); ucd.setUpperCase(true); t1.setEditable(false); } } public static void main(String[] args) { Console.run(new TextNew(), 375, 125); } } class UpperCaseDocument extends PlainDocument { boolean upperCase = true; public void setUpperCase(boolean flag) { upperCase = flag; } public void insertString(int offset, String string, AttributeSet attributeSet) throws BadLocationException { if(upperCase) string = string.toUpperCase(); super.insertString(offset, string, attributeSet); } } ///:~
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 reference 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.[65]
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:
//: c13:TextAreaNew.java // Controlling scrollbars with JtextArea. // <applet code=TextAreaNew width=300 height=725> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class TextAreaNew extends JApplet { JButton b1 = new JButton("Text Area 1"), b2 = new JButton("Text Area 2"), b3 = new JButton("Replace Text"), b4 = new JButton("Insert Text"); JTextArea t1 = new JTextArea("t1", 1, 20), t2 = new JTextArea("t2", 4, 20), t3 = new JTextArea("t3", 1, 20), t4 = new JTextArea("t4", 10, 10), t5 = new JTextArea("t5", 4, 20), t6 = new JTextArea("t6", 10, 10); JScrollPane sp3 = new JScrollPane(t3, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp4 = new JScrollPane(t4, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER), sp5 = new JScrollPane(t5, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS), sp6 = new JScrollPane(t6, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS); 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 void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); // Create Borders for components: Border brd = BorderFactory.createMatteBorder( 1, 1, 1, 1, Color.black); t1.setBorder(brd); t2.setBorder(brd); sp3.setBorder(brd); sp4.setBorder(brd); sp5.setBorder(brd); sp6.setBorder(brd); // Initialize listeners and add components: b1.addActionListener(new B1L()); cp.add(b1); cp.add(t1); b2.addActionListener(new B2L()); cp.add(b2); cp.add(t2); b3.addActionListener(new B3L()); cp.add(b3); b4.addActionListener(new B4L()); cp.add(b4); cp.add(sp3); cp.add(sp4); cp.add(sp5); cp.add(sp6); } public static void main(String[] args) { Console.run(new TextAreaNew(), 300, 725); } } ///:~
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:
//: c13:RadioCheckNew.java // Radio buttons and Check Boxes. // <applet code=RadioCheckNew // width=325 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class RadioCheckNew extends JApplet { JTextField t = new JTextField(20); JCheckBox[] cb = { new JCheckBox("Check Box 1"), new JCheckBox("Check Box 2"), new JCheckBox("Check Box 3") }; ButtonGroup group = new ButtonGroup(); JRadioButton cb4 = new JRadioButton("four"), cb5 = new JRadioButton("five"), cb6 = new JRadioButton("six"); // 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 void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); t.setEditable(false); cp.add(t); ILCheck il = new ILCheck(); for(int i = 0; i < cb.length; i++) { cb[i].addItemListener(il); cp.add(cb[i]); } group.add(cb4); group.add(cb5); group.add(cb6); cb4.addItemListener(new IL4()); cb5.addItemListener(new IL5()); cb6.addItemListener(new IL6()); cp.add(cb4); cp.add(cb5); cp.add(cb6); } public static void main(String[] args) { Console.run(new RadioCheckNew(), 325, 100); } } ///:~
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:
//: c13:ChoiceNew.java // Drop-down lists (combo boxes). // <applet code=ChoiceNew // width=450 height=175></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class ChoiceNew extends JApplet { String[] descriptions1 = { "Ebullient", "Obtuse", "Recalcitrant", "Brilliant" }; String[] descriptions2 = { "Somnescent", "Timorous", "Florid", "Putrescent" }; JTextArea t = new JTextArea(7, 40); JComboBox c = new JComboBox(descriptions1); JButton b = new JButton("Add items"); int count = 0; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); t.setLineWrap(true); t.setEditable(false); cp.add(t); cp.add(c); cp.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); } } class BL implements ActionListener { public void actionPerformed(ActionEvent e) { if(count < descriptions2.length) c.addItem(descriptions2[count++]); if(count >=descriptions2.length) b.setEnabled(false); } } public static void main(String[] args) { Console.run(new ChoiceNew(), 450, 175); } } ///:~
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:
//: c13:ListNew.java // <applet code=ListNew width=250 // height=325> </applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class ListNew extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; DefaultListModel lItems=new DefaultListModel(); JList lst = new JList(lItems); JTextArea t = new JTextArea(flavors.length,20); JButton b = new JButton("Add Item"); ActionListener bl = new ActionListener() { public void actionPerformed(ActionEvent e) { if(count < flavors.length) { lItems.add(0, flavors[count++]); } else { // Disable, since there are no more // flavors left to be added to the List b.setEnabled(false); } } }; ListSelectionListener ll = new ListSelectionListener() { public void valueChanged( ListSelectionEvent e) { t.setText(""); Object[] items=lst.getSelectedValues(); for(int i = 0; i < items.length; i++) t.append(items[i] + "\n"); } }; int count = 0; public void init() { Container cp = getContentPane(); t.setEditable(false); cp.setLayout(new FlowLayout()); // Create Borders for components: Border brd = BorderFactory.createMatteBorder( 1, 1, 2, 2, Color.black); lst.setBorder(brd); t.setBorder(brd); // Add the first four items to the List for(int i = 0; i < 4; i++) lItems.addElement(flavors[count++]); // Add items to the Content Pane for Display cp.add(t); cp.add(lst); cp.add(b); // Register event listeners lst.addListSelectionListener(ll); b.addActionListener(bl); } public static void main(String[] args) { Console.run(new ListNew(), 250, 325); } } ///:~
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.
//: c13:MenuNew.java // Menu shortcuts and action commands. // <applet code=MenuNew width=300 // height=100> </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class MenuNew extends JApplet { String[] flavors = { "Chocolate", "Strawberry", "Vanilla Fudge Swirl", "Mint Chip", "Mocha Almond Fudge", "Rum Raisin", "Praline Cream", "Mud Pie" }; JTextField t = new JTextField("No flavor", 30); JMenuBar mb1 = new JMenuBar(); JMenu f = new JMenu("File"), m = new JMenu("Flavors"), s = new JMenu("Safety"); // Alternative approach: JCheckBoxMenuItem[] safety = { new JCheckBoxMenuItem("Guard"), new JCheckBoxMenuItem("Hide") }; JMenuItem[] file = { // No menu shortcut: new JMenuItem("Open"), // Adding a menu shortcut is very simple: new JMenuItem("Exit", KeyEvent.VK_E) }; // A second menu bar to swap to: JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { new JMenuItem("Foo"), new JMenuItem("Bar"), new JMenuItem("Baz"), }; JButton b = new JButton("Swap Menus"); class BL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuBar m = getJMenuBar(); setJMenuBar(m == mb1 ? mb2 : mb1); validate(); // Refresh the frame ///////// Necessary??? } } class ML implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)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")) { // This trick won't work with applets // because MenuNew.this doesn't produce // a Window if this is a JApplet: //dispatchEvent( // new WindowEvent(MenuNew.this, // WindowEvent.WINDOW_CLOSING)); } } } class FL implements ActionListener { public void actionPerformed(ActionEvent e) { JMenuItem target = (JMenuItem)e.getSource(); t.setText(target.getText()); } } // 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) { JCheckBoxMenuItem target = (JCheckBoxMenuItem)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 void init() { 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()); FL fl = new FL(); for(int i = 0; i < flavors.length; i++) { JMenuItem mi = new JMenuItem(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); setJMenuBar(mb1); t.setEditable(false); Container cp = getContentPane(); cp.add(t, BorderLayout.CENTER); // Set up the system for swapping menus: b.addActionListener(new BL()); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); mb2.add(fooBar); } public static void main(String[] args) { Console.run(new MenuNew(), 300, 100); } } ///:~
This code is similar to the previous
(Java 1.0) version, until you get to the init( )
method. 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 reference 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
TicTacToe.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 TicTacToe.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.
//: c13:TicTacToeInner.java // TicTacToe.java with heavy use of inner classes. // <applet code=TicTacToeInner // width=200 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class TicTacToeInner extends JApplet { JTextField rows = new JTextField("3"), cols = new JTextField("3"); static final int BLANK = 0, XX = 1, OO = 2; class ToeDialog extends JDialog { int turn = XX; // Start with x's turn // w = number of cells wide // h = number of cells high public ToeDialog(int w, int h) { setTitle("The game itself"); Container cp = getContentPane(); cp.setLayout(new GridLayout(w, h)); for(int i = 0; i < w * h; i++) cp.add(new ToeButton()); setSize(w * 50, h * 50); addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e){ dispose(); } }); } class ToeButton extends JPanel { int state = BLANK; public ToeButton() { addMouseListener(new ML()); } public void paintComponent(Graphics g) { super.paintComponent(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) { JDialog d = new ToeDialog( Integer.parseInt(rows.getText()), Integer.parseInt(cols.getText())); d.setVisible(true); } } public void init() { JPanel p = new JPanel(); p.setLayout(new GridLayout(2,2)); p.add(new JLabel("Rows", JLabel.CENTER)); p.add(rows); p.add(new JLabel("Columns", JLabel.CENTER)); p.add(cols); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); JButton b = new JButton("go"); b.addActionListener(new BL()); cp.add(b, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new TicTacToeInner(), 200, 100); } } ///:~
Because statics can be at only the
outer level of the class, inner classes cannot have static data or
static inner classes.
One of the very interesting aspects of
Swing is the “Pluggable Look & Feel.” This allows your program
to emulate the look and feel of various operating environments. You can even do
all sorts of fancy things like dynamically changing the look and feel while the
program is executing. However, you generally just want to do one of two things,
either select the “cross platform” look and feel (which is
Swing’s “metal”) or select the look and feel for the system
you are currently on, so your Java program looks like it was created
specifically for that system. The code to select either of these behaviors is
quite simple, but you must execute it before you create any visual
components because the components will be made based on the current look and
feel and will not be changed just because you happen to change the look and feel
midway during the program (that process is more complicated and uncommon, and is
relegated to Swing-specific books).
Actually, if you want to use the
cross-platform (“metal”) look and feel that is characteristic of
Swing programs, you don’t have to do anything—it’s the
default. But if you want instead to use the current operating
environment’s look and feel, you just insert the following code, typically
at the beginning of your main( ) but somehow before any components
are added:
try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch (Exception ex) { }
You don’t need anything in the
catch clause because the UIManager will default to the
cross-platform look and feel if your attempts to set up any of the alternatives
fail. However, during debugging the exception can be quite
useful.
Here is a program that takes a
command-line argument to select a look and feel, and shows how a selection of
components look:
//: c13:LookAndFeel.java // Selecting different looks & feels. import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class LookAndFeel extends JFrame { String[] choices = { "eeny", "meeny", "minie", "moe", "toe", "you" }; Component[] samples = { new JButton("JButton"), new JTextField("JTextField"), new JLabel("JLabel"), new JCheckBox("JCheckBox"), new JRadioButton("Radio"), new JComboBox(choices), new JList(choices), }; public LookAndFeel() { super("Look And Feel"); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); for(int i = 0; i < samples.length; i++) cp.add(samples[i]); } private static void usageError() { System.out.println( "Usage:LookAndFeel [cross|system|motif]"); System.exit(1); } public static void main(String[] args) { if(args.length == 0) usageError(); if(args[0].equals("cross")) { try { UIManager.setLookAndFeel(UIManager. getCrossPlatformLookAndFeelClassName()); } catch (Exception ex) { ex.printStackTrace(); } } else if(args[0].equals("system")) { try { UIManager.setLookAndFeel(UIManager. getSystemLookAndFeelClassName()); } catch (Exception ex) { ex.printStackTrace(); } } else if(args[0].equals("motif")) { try { UIManager.setLookAndFeel("com.sun.java."+ "swing.plaf.motif.MotifLookAndFeel"); } catch (Exception ex) { ex.printStackTrace(); } } else usageError(); // Note the look & feel must be set before // any components are created. Console.run(new LookAndFeel(), 300, 200); } } ///:~
You can see that one option is to
explicitly specify a string for a look and feel, as seen with
MotifLookAndFeel. However, that one and the default “metal”
look and feel are the only ones that can legally be used on any platform; even
though there are strings for Windows and Macintosh look and feels, those can
only be used on their respective platforms (these are produced when you call
getSystemLookAndFeelClassName( ) and you’re on that particular
platform).
It is also possible to create a custom
look and feel package, for example if you are building a framework for a company
that wants a distinctive appearance. This is a big job and is far beyond the
scope of this book (in fact, you’ll discover it is beyond the scope of
many dedicated Swing books!).
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:
//: c13:DynamicEvents.java // You can change event behavior dynamically. // Also shows multiple actions for an event. // <applet code=DynamicEvents // width=200 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class DynamicEvents extends JApplet { ArrayList v = new ArrayList(); int i = 0; JButton b1 = new JButton("Button1"), b2 = new JButton("Button2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); b1.addActionListener(new B()); b1.addActionListener(new B1()); b2.addActionListener(new B()); b2.addActionListener(new B2()); cp.add(b1); cp.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.add(a); b2.addActionListener(a); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("Button2 pressed"); int end = v.size() - 1; if(end >= 0) { b2.removeActionListener( (ActionListener)v.get(end)); v.remove(end); } } } public static void main(String[] args) { Console.run(new DynamicEvents(), 200, 100); } } ///:~
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:
//: c13:Separation.java // Separating GUI logic and business objects. // <applet code=Separation // width=250 height=100> </applet> import javax.swing.*; import java.awt.*; import javax.swing.event.*; import java.awt.event.*; import java.applet.*; import com.bruceeckel.swing.*; class BusinessLogic { private int modifier; public 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 JApplet { JTextField t = new JTextField(15), mod = new JTextField(15); BusinessLogic bl = new BusinessLogic(2); JButton calc1 = new JButton("Calculation 1"), calc2 = new JButton("Calculation 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); calc1.addActionListener(new Calc1L()); calc2.addActionListener(new Calc2L()); JPanel p1 = new JPanel(); p1.add(calc1); p1.add(calc2); cp.add(p1); mod.getDocument(). addDocumentListener(new ModL()); JPanel p2 = new JPanel(); p2.add(new JLabel("Modifier:")); p2.add(mod); cp.add(p2); } static int getValue(JTextField 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)))); } } // If you want something to happen whenever // a JTextField changes, add this listener: class ModL implements DocumentListener { public void changedUpdate(DocumentEvent e) {} public void insertUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } public void removeUpdate(DocumentEvent e) { bl.setModifier(getValue(mod)); } } public static void main(String[] args) { Console.run(new Separation(), 250, 100); } } ///:~
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:
//: c13:GoodIdea.java // The best way to design classes using the // Java event model: use an inner class for // each different event. This maximizes // flexibility and modularity. // <applet code=GoodIdea // width=200 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class GoodIdea extends JApplet { JButton b1 = new JButton("Button 1"), b2 = new JButton("Button 2"); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); b1.addActionListener(new B1L()); b2.addActionListener(new B2L()); cp.add(b1); cp.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) { Console.run(new GoodIdea(), 200, 100); } } ///:~
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.
JFC includes
important functionality, including focus traversal, desktop color access,
printing “inside the sandbox,” the beginnings of clipboard support,
and drag-and-drop.
Focus
traversal is quite easy, since it’s transparently present in Swing 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).
The JFC 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. 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.
//: c13:CutAndPaste.java // Using the clipboard. // <applet code=CutAndPaste // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import com.bruceeckel.swing.*; public class CutAndPaste extends JApplet { JMenuBar mb = new JMenuBar(); JMenu edit = new JMenu("Edit"); JMenuItem cut = new JMenuItem("Cut"), copy = new JMenuItem("Copy"), paste = new JMenuItem("Paste"); JTextArea text = new JTextArea(20, 20); Clipboard clipbd = getToolkit().getSystemClipboard(); public void init() { cut.addActionListener(new CutL()); copy.addActionListener(new CopyL()); paste.addActionListener(new PasteL()); edit.add(cut); edit.add(copy); edit.add(paste); mb.add(edit); setJMenuBar(mb); getContentPane().add(text); } class CopyL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; StringSelection clipString = new StringSelection(selection); clipbd.setContents(clipString,clipString); } } class CutL implements ActionListener { public void actionPerformed(ActionEvent e) { String selection = text.getSelectedText(); if (selection == null) return; 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 evt) { System.out.println("not String flavor"); } } } public static void main(String[] args) { Console.run(new CutAndPaste(), 300, 200); } } ///:~
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 palette 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 Thinking in Patterns with Java,
downloadable at www.BruceEckel.com) 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:
//: frogbean: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 12: 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 reference 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):
//: c13: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); Method writeMethod = properties[i].getWriteMethod(); if(writeMethod != null) System.out.println( "Write method:\n " + writeMethod); 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()); 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()); Method addListener = events[i].getAddListenerMethod(); System.out.println( "Add Listener Method:\n " + addListener); Method removeListener = events[i].getRemoveListenerMethod(); System.out.println( "Remove Listener Method:\n " + removeListener); 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
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:BangBean.java // A graphical Bean. package bangbean; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.io.*; import java.util.*; import com.bruceeckel.swing.*; public class BangBean extends JPanel 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 paintComponent(Graphics g) { super.paintComponent(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); } // During testing, send action // information to the console: static class BBL implements ActionListener { public void actionPerformed(ActionEvent e) { System.out.println("BangBean action"); } } // Testing the BangBean: public static void main(String[] args) { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) {} Console.run(bb, 300, 300); } } ///:~
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 reinvoke 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
PropertyVetoException to
prevent the change from happening and to restore the old
values.
You can also change the way your Bean is
represented at design time:
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 Swing (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 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 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:
//: c13:ButtonDemo.java // Looks like Java 1.1 but with J's added. // <applet code=ButtonDemo width=250 height=75> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.applet.*; import com.bruceeckel.swing.*; public class ButtonDemo extends JApplet { JButton b1 = new JButton("JButton 1"), b2 = new JButton("JButton 2"); JTextField t = new JTextField(20); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); t.setText(name + " Pressed"); } }; b1.addActionListener(al); cp.add(b1); b2.addActionListener(al); cp.add(b2); cp.add(t); } public static void main(String[] args) { Console.run(new ButtonDemo(), 250, 75); } } ///:~
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
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:
//: c13:Borders.java // Different Swing borders. // <applet code=Borders // width=500 height=300></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Borders extends JApplet { 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 void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.setLayout(new GridLayout(2,4)); cp.add(showBorder(new TitledBorder("Title"))); cp.add(showBorder(new EtchedBorder())); cp.add(showBorder(new LineBorder(Color.blue))); cp.add(showBorder( new MatteBorder(5,5,30,30,Color.green))); cp.add(showBorder( new BevelBorder(BevelBorder.RAISED))); cp.add(showBorder( new SoftBevelBorder(BevelBorder.LOWERED))); cp.add(showBorder(new CompoundBorder( new EtchedBorder(), new LineBorder(Color.red)))); } public static void main(String[] args) { Console.run(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:
//: c13:Buttons.java // Various Swing buttons. // <applet code=Buttons // width=350 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.plaf.basic.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Buttons extends JApplet { 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 void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(jb); cp.add(new JToggleButton("JToggleButton")); cp.add(new JCheckBox("JCheckBox")); cp.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); cp.add(jp); } public static void main(String[] args) { Console.run(new Buttons(), 350, 100); } } ///:~
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:
//: c13:ButtonGroups.java // Uses reflection to create groups // of different types of AbstractButton. // <applet code=ButtonGroups // width=500 height=300></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.border.*; import java.lang.reflect.*; import com.bruceeckel.swing.*; public class ButtonGroups extends JApplet { 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 void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(makeBPanel(JButton.class, ids)); cp.add(makeBPanel(JToggleButton.class, ids)); cp.add(makeBPanel(JCheckBox.class, ids)); cp.add(makeBPanel(JRadioButton.class, ids)); } public static void main(String[] args) { Console.run(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 this 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.
//: c13:Faces.java // Icon behavior in Jbuttons. // <applet code=Faces // width=400 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Faces extends JApplet { // The following path information is necessary // to run via an applet directly from the disk: static String path = "C:/aaa-TIJ2-distribution/code/c13/"; static Icon[] faces = { new ImageIcon(path + "face0.gif"), new ImageIcon(path + "face1.gif"), new ImageIcon(path + "face2.gif"), new ImageIcon(path + "face3.gif"), new ImageIcon(path + "face4.gif"), }; JButton jb = new JButton("JButton", faces[3]), jb2 = new JButton("Disable"); boolean mad = false; public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); 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!"); cp.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"); } } }); cp.add(jb2); } public static void main(String[] args) { Console.run(new Faces(), 400, 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.
//: c13:Menus.java // A menu-building system; also demonstrates // icons in labels and menu items. // <applet code=Menus // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Menus extends JApplet { 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; } public void init() { Container cp = getContentPane(); cp.add(createMenuBar(menuBar), BorderLayout.NORTH); JPanel p = new JPanel(); p.setLayout(new BorderLayout()); p.add(t, BorderLayout.NORTH); p.add(l, BorderLayout.CENTER); cp.add(p, BorderLayout.CENTER); } public static void main(String[] args) { Console.run(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 references 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
references 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:
//: c13:Popup.java // Creating popup menus with Swing. // <applet code=Popup // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Popup extends JApplet { JPopupMenu popup = new JPopupMenu(); JTextField t = new JTextField(10); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.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) { Console.run(new Popup(), 300, 200); } } ///:~
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:
//: c13:ListCombo.java // List boxes & Combo boxes. // <applet code=ListCombo // width=300 height=125></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class ListCombo extends JApplet { public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(2,1)); JList list = new JList(ButtonGroups.ids); cp.add(new JScrollPane(list)); JComboBox combo = new JComboBox(); for(int i = 0; i < 100; i++) combo.addItem(Integer.toString(i)); cp.add(combo); } public static void main(String[] args) { Console.run(new ListCombo(), 300, 125); } } ///:~
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:
//: c13:Progress.java // Using progress bars and sliders. // <applet code=Progress // width=300 height=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.event.*; import javax.swing.border.*; import com.bruceeckel.swing.*; public class Progress extends JApplet { JProgressBar pb = new JProgressBar(); JSlider sb = new JSlider(JSlider.HORIZONTAL, 0, 100, 60); public void init() { Container cp = getContentPane(); cp.setLayout(new GridLayout(2,1)); cp.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 cp.add(sb); } public static void main(String[] args) { Console.run(new Progress(), 300, 200); } } ///:~
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):
//: c13:Trees.java // Simple Swing tree example. Trees can // be made vastly more complex than this. // <applet code=Trees // width=250 height=250></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.tree.*; import com.bruceeckel.swing.*; // 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 JApplet { 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 void init() { Container cp = getContentPane(); root = new DefaultMutableTreeNode("root"); tree = new JTree(root); // Add it and make it take care of scrolling: cp.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); cp.add(p, BorderLayout.SOUTH); } public static void main(String[] args) { Console.run(new Trees(), 250, 250); } } ///:~
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:
//: c13:Table.java // Simple demonstration of JTable. // <applet code=Table // width=350 height=100></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.table.*; import javax.swing.event.*; import com.bruceeckel.swing.*; // 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(); } } } public 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 JApplet { public void init() { Container cp = getContentPane(); JTable table = new JTable(new DataModel()); JScrollPane scrollpane = JTable.createScrollPaneForTable(table); cp.add(scrollpane, BorderLayout.CENTER); } public static void main(String[] args) { Console.run(new Table(), 350, 100); } } ///:~
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.
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.
It’s possible for an applet to
cause the display of any URL through the Web browser the applet is running
within. You can do this with the following line:
getAppletContext().showDocument(u);
in
which u is the URL object. Here’s a simple example that
redirects you to another Web page. The page happens to be the output of a CGI
program, but you can as easily go to an ordinary HTML page, so you could build
on this applet to produce a password-protected gateway to a particular portion
of your Web site:
//: c13:ShowHTML.java // <applet code=ShowHTML width=100 height=50> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*; public class ShowHTML extends JApplet { static final String CGIProgram = "MyCGIProgram"; JButton send = new JButton("Go"); JLabel l = new JLabel(); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); send.addActionListener(new Al()); cp.add(send); cp.add(l); } class Al implements ActionListener { public void actionPerformed(ActionEvent ae) { try { // This could be an HTML page instead of // a CGI program. Notice that this CGI // program doesn't use arguments, but // you can add them in the usual way. URL u = new URL( getDocumentBase(), "cgi-bin/" + CGIProgram); // Display the output of the URL using // the Web browser, as an ordinary page: getAppletContext().showDocument(u); } catch(Exception e) { l.setText(e.toString()); } } } public static void main(String[] args) { Console.run(new ShowHTML(), 100, 50); } } ///:~
The beauty of the
URL class is how much it
shields you from. You can connect to Web servers without knowing much at all
about what’s going on under the
covers.
A variation on the above program reads a
file located on the server. The file is specified by the
client:
//: c13:Fetcher.java // <applet code=Fetcher width=500 height=300> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.net.*; import java.io.*; import com.bruceeckel.swing.*; public class Fetcher extends JApplet { JButton fetchIt= new JButton("Fetch the Data"); JTextField f = new JTextField("Fetcher.java", 20); JTextArea t = new JTextArea(10,40); public void init() { Container cp = getContentPane(); cp.setLayout(new FlowLayout()); fetchIt.addActionListener(new FetchL()); cp.add(new JScrollPane(t)); cp.add(f); cp.add(fetchIt); } public class FetchL implements ActionListener { public void actionPerformed(ActionEvent e) { try { URL url = new URL(getDocumentBase(),f.getText()); t.setText(url + "\n"); InputStream is = url.openStream(); BufferedReader in = new BufferedReader( new InputStreamReader(is)); String line; while ((line = in.readLine()) != null) t.append(line + "\n"); } catch(Exception ex) { t.append(ex.toString()); } } } public static void main(String[] args) { Console.run(new Fetcher(), 500, 300); } } ///:~
To finish the chapter, we’ll
develop a useful application that allows you to quickly look up methods for
classes, as a programming aid.
Chapter 12
introduced the Java 1.1 concept of
reflection and used that
feature to look up methods for a particular class—either the entire list
of methods or a subset of those whose names match a keyword you provide. The
magic of this is that it can automatically show you all the methods for a
class without forcing you to walk up the inheritance hierarchy examining the
base classes at each level. Thus, it provides a valuable timesaving tool for
programming: because the names of most Java method names are made nicely verbose
and descriptive, you can search for the method names that contain a particular
word of interest. When you find what you think you’re looking for, check
the online documentation.
However, by Chapter 12 you hadn’t
seen Swing, so that tool was developed as a command-line application. Here is
the more useful GUI version, which dynamically updates the output as you type
and also allows you to cut and paste from the output:
//: c13:DisplayMethods.java // Display the methods of any class inside // a window. Dynamically narrows your search. // <applet code = DisplayMethods // width=650 height=700></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.lang.reflect.*; import java.io.*; import com.bruceeckel.swing.*; public class DisplayMethods extends JApplet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; JTextField name = new JTextField(40), searchFor = new JTextField(30); JCheckBox strip = new JCheckBox("Strip Qualifiers", true); JTextArea results = new JTextArea(40, 65); class NameL implements ActionListener { public void actionPerformed(ActionEvent e) { String nm = name.getText().trim(); if(nm.length() == 0) { results.setText("No match"); n = new String[0]; return; } try { cl = Class.forName(nm); } catch (ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); ctor = cl.getConstructors(); // Convert to an array of Strings: n = new String[m.length + ctor.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); for(int i = 0; i < ctor.length; i++) n[i + m.length] = ctor[i].toString(); reDisplay(); } } class StripL implements ItemListener { public void itemStateChanged(ItemEvent e) { reDisplay(); } } class SearchForL implements ActionListener { public void actionPerformed(ActionEvent e) { reDisplay(); } } void reDisplay() { // Create the result set: String[] rs = new String[n.length]; String find = searchFor.getText(); int j = 0; // Select from the list if find exists: for (int i = 0; i < n.length; i++) { if(find == null) rs[j++] = n[i]; else if(n[i].indexOf(find) != -1) rs[j++] = n[i]; } results.setText(""); if(strip.isSelected()) for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); else // Leave qualifiers on for (int i = 0; i < j; i++) results.append(rs[i] + "\n"); } public void init() { name.addActionListener(new NameL()); searchFor.addActionListener(new SearchForL()); strip.addItemListener(new StripL()); JPanel top = new JPanel(), lower = new JPanel(), p = new JPanel(new BorderLayout()); top.add(new JLabel("Qualified class name:")); top.add(name); lower.add( new JLabel("String to search for:")); lower.add(searchFor); lower.add(strip); p.add(top, BorderLayout.NORTH); p.add(lower, BorderLayout.SOUTH); Container cp = getContentPane(); cp.add(p, BorderLayout.NORTH); cp.add(results, BorderLayout.CENTER); } public static void main(String[] args) { Console.run(new DisplayMethods(), 650, 700); } } class StripQualifiers { private StreamTokenizer st; public StripQualifiers(String qualified) { st = new StreamTokenizer( new StringReader(qualified)); st.ordinaryChar(' '); } public String getNext() { String s = null; try { if(st.nextToken() != StreamTokenizer.TT_EOF) { switch(st.ttype) { case StreamTokenizer.TT_EOL: s = null; break; case StreamTokenizer.TT_NUMBER: s = Double.toString(st.nval); break; case StreamTokenizer.TT_WORD: s = new String(st.sval); break; default: // single character in ttype s = String.valueOf((char)st.ttype); } } } catch(IOException e) { System.out.println(e); } return s; } public static String strip(String qualified) { StripQualifiers sq = new StripQualifiers(qualified); String s = "", si; while((si = sq.getNext()) != null) { int lastDot = si.lastIndexOf('.'); if(lastDot != -1) si = si.substring(lastDot + 1); s += si; } return s; } } ///:~
Some things you’ve seen before. As
with many of the GUI programs in this book, this is created to perform both as
an application and as an applet. Also, the StripQualifiers class is
exactly the same as it was in Chapter 12.
The GUI contains a TextField
name in which you can enter the fully-qualified class name you want to
look up, and another one, searchFor, in which you can enter the optional
text to search for within the list of methods. The Checkbox allows you to
say whether you want to use the fully-qualified names in the output or if you
want the qualification stripped off. Finally, the results are displayed in a
TextArea.
You’ll notice that there are no
buttons or other components by which to indicate that you want the search to
start. That’s because both of the TextFields and the
Checkbox are monitored by their listener objects. Whenever you make a
change, the list is immediately updated. If you change the text within the
name field, the new text is captured in class NameL. If the text
isn’t empty, it is used inside
Class.forName( ) to
try to look up the class. As you’re typing, of course, the name will be
incomplete and Class.forName( ) will fail, which means that it
throws an exception. This is trapped and the TextArea is set to “No
match”. But as soon as you type in a correct name (capitalization counts),
Class.forName( ) is successful and getMethods( ) and
getConstructors( ) will return arrays of Method and
Constructor objects, respectively. Each of the objects in these arrays is
turned into a String via toString( ) (this produces the
complete method or constructor signature) and both lists are combined into
n, a single String array. The array n is a member of
class DisplayMethods and is used in updating the display whenever
reDisplay( ) is called.
If you change the Checkbox or
searchFor components, their listeners simply call
reDisplay( ). reDisplay( ) creates a temporary array of
String called rs (for “result set”). The result set is
either copied directly from n if there is no find word, or
conditionally copied from the Strings in n that contain the
find word. Finally, the strip Checkbox is interrogated to
see if the user wants the names to be stripped (the default is
“yes”). If so, StripQualifiers.strip( ) does the job; if
not, the list is simply displayed.
In init( ), you might think
that there’s a lot of busy work involved in setting up the layout. In
fact, it is possible to lay out the components with less work, but the advantage
of using BorderLayouts this way is that it allows the user to resize the
window and make—in particular—the TextArea larger, which
means you can resize to allow you to see longer names without
scrolling.
You might find that you’ll keep
this tool running while you’re programming, since it provides one of the
best “first lines of attack” when you’re trying to figure out
what method to call.
Of all the libraries in Java, the GUI
library has seen the most dramatic changes from Java 1.0
to Java 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.
[59]
A varation on this is called “the principle of least astonishment,”
which essentially says: “don’t surprise the
user.”
[60]
This is an example of the design pattern called the template
method.
[61]
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.
[62]
ShowStatus( ) is also a method of Applet, so you can call it
directly, without calling getAppletContext( ).
[63]
This behavior is apparently a bug and will be fixed in a later version of
Java.
[64]
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.
[65]
It also solves the problem of “callbacks” without adding any awkward
“method pointer” feature to Java.
[66]
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.