A fundamental design guideline is
“make simple things easy, and difficult things
possible[62].”
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.
The situation improved with the Java 1.1
AWT event model, which takes a much clearer, object-oriented approach, along
with the addition of JavaBeans, 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 essentially replacing
everything with 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 JavaBeans 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 you have browser limitations), you can find that introduction in the online
first edition of this book at www.BruceEckel.com (also included on the CD
ROM bound with this book).
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 either all the Swing components, or all the methods
for the described classes. What you see here is intended to be simple. 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. If you need to do more, then
Swing can probably give you what you want if you’re willing to do the
research.
I assume here that you have downloaded
and installed the (free) Java library documents in HTML format from
java.sun.com and will browse the javax.swing classes in that
documentation to see the full details and methods of the Swing library. Because
of the simplicity of the Swing design, 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 more depth, or if you
want to modify the default Swing behavior.
Swing contains all the components that you expect to see in a modern UI, everything from buttons that contain pictures to trees and tables. 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 complex things, your code becomes proportionally more complex. This means an easy entry point, but you’ve got the power if you need it.
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 standard naming conventions,
much of the time that 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.
For speed, all the
components are “lightweight,” and Swing is
written entirely in Java for portability.
Keyboard
navigation is automatic—you can run a Swing application without using the
mouse, but this doesn’t require any extra programming. Scrolling support
is effortless—you simply wrap your component in a JScrollPane as
you add it to your form. Features such as tool tips typically require a single
line of code to use.
Swing also supports a rather radical
feature 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 (albeit difficult) to invent your own look and
feel.
One of Java’s 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 support 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—that is, the Java run-time
security system—watching over you.
However, you can also step outside the
sandbox and write regular applications rather than applets, 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.
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:
If you can live within the restrictions,
applets have definite advantages, especially when building
client/server or other networked applications:
Because applets
are automatically integrated with HTML, you have a built-in platform-independent
documentation system to support the applet. It’s an interesting twist,
since we’re used to having the documentation part of the program rather
than vice versa.
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 create other classes. A certain category 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[63].
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( ) |
Automatically called 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 are ready to
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 into the application
framework; you put any startup code in init( ).
In this program, the only activity is
putting a text label on the applet, via the
JLabel class (the old AWT appropriated the name
Label as well as other names of components, so you will often see a
leading ‘J’ used with Swing components). The constructor for
this class takes a String and uses it to create the label. In the above
program this label is placed on the form.
The
init( ) method is responsible for putting all the components on the
form using the add( ) method. You might think that you ought to be
able to simply call add( ) by itself, and in fact that’s the
way it used to be in the old AWT. However, Swing requires you to add all
components to the “content pane” of a form, and so you must call
getContentPane( ) as part of the add( )
process.
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[64] to tell
the page how to load and run the applet.
This process used to be very simple, when
Java itself 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 it 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[65]:
//:! 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">
and there can be as many as you
want.
The source code package for this book
provides an HTML page for each of the applets in this book, and thus many
examples of the applet tag. You can find a full and current description of the
details of placing applets in Web pages at
java.sun.com.
Sun’s JDK (freely downloadable from
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. 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
In this book, this form will be used for
easy testing of applets. Shortly, 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. You must also have a way
to move the HTML files and the .class files from your site to the correct
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 during your testing process because the browser finds it on
your machine. When someone else connects, however, his or her browser
can’t find it. So when you’re testing, make sure you erase the
relevant .class files (or .jar file) on your local machine to
verify that they exist in the proper location on the server.
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.
There are times when you’d like to
make a windowed program do something else other than sit on a Web page. Perhaps
you’d also like it to do some of the things a “regular”
application can do but still 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.
The 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[66] 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
JFrame[67].
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 Applet1c(); JFrame frame = new JFrame("Applet1c"); // 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. The applet is
created and added to a JFrame so that it can be displayed.
The line:
Console.setupClosing(frame);
Causes the window to be properly closed.
Console comes from com.bruceeckel.swing and will be explained a
little later.
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 force the calls
yourself[68].
Notice the last line:
frame.setVisible(true);
Although the code that turns programs into both applets and applications produces valuable results, if used everywhere it becomes distracting and wastes paper. Instead, the following display framework will be used for the Swing examples in the rest of this book: //: 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); } } ///:~
This is a tool you may want to use
yourself, so it’s placed in the library
com.bruceeckel.swing. The Console class
consists entirely of static methods. The first is used to extract the
class name (using RTTI) from any object and to remove the word
“class” which is typically prepended by getClass( ).
This uses the String methods indexOf( ) to determine whether
the word “class” is there, and substring( ) to produce
the new string without “class” or the trailing space. This name is
used to label the window that is displayed by the run( )
methods.
setupClosing( ) is used to
hide the code that causes a JFrame to exit a program when that
JFrame is closed. The default behavior is to do nothing, so if you
don’t call setupClosing( ) or write the equivalent code for
your JFrame, the application won’t close.
The reason this code is hidden rather than placing it directly in the subsequent
run( ) methods is partly because it allows you to use the method by
itself when what you want to do is more complicated than what run( )
provides. However, it also isolates a change factor: Java 2 has two ways of
causing certain types of windows to close. In JDK 1.2, the solution is to create
a new WindowAdapter class and implement
windowClosing( ), as seen above (the meaning
of this will be fully explained later in this Chapter). However, during the
creation of JDK 1.3 the library designers observed that you typically need to
close windows whenever you’re creating a non-applet, and so they added the
setDefaultCloseOperation( ) to JFrame
and JDialog. From the standpoint of writing code, the new method is much
nicer to use but this book was written while there was still no JDK 1.3
implemented on Linux and other platforms, so in the interest of cross-version
portability the change was isolated inside
setupClosing( ).
The run( ) method is
overloaded to work with JApplets, JPanels, and JFrames.
Note that only if it’s a JApplet are init( ) and
start( ) called.
Now any applet can be run from the
console by creating a main( ) containing 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 Applet1d(), 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—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 and language documentation on that
site.
Next, create the following script without
the first and last lines (this script 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.” For “Associated Extension,” enter “class.” 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"
You must customize the path before
“RunJava.bat” to conform to the location where you placed the batch
file.
Once you perform this installation, you
may run any Java program by simply double-clicking on the .class file
containing a
main( ).
Making a button is quite simple: you just
call the JButton constructor with the label you want on the button.
You’ll see later that you can do fancier things, like putting graphic
images on buttons.
Usually you’ll want to create a
field for the button inside your class so that you can refer to it
later.
The JButton is a
component—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 do it inside init( ):
//: 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); } } ///:~
Something new has been added
here—before any elements are placed on the content pane, it is given a new
“layout manager,” of type FlowLayout. The layout manager is
the way that the pane implicitly decides where to place the control on the form.
The normal behavior of an applet is to use the BorderLayout, but that
won’t work here because (as you will learn later in this chapter when
controlling the layout of a form is examined in more detail) it defaults to
covering each control entirely with every new one that is added. However,
FlowLayout causes the controls to flow evenly onto the form, left to
right and top to
bottom.
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.
The way that this is accomplished in Swing is by cleanly separating the interface (the graphical components) and the implementation (the code that you want to run when an event happens to a component). Each Swing component can report all the events that might happen to it, and it can report each kind of event individually. So if you’re not interested in, for example, whether the mouse is being moved over your button, you don’t register your interest in that event. It’s a very straightforward and elegant way to handle event-driven programming, and once you understand the basic concepts you can easily use Swing components that you haven’t seen before—in fact, this model extends to anything that can be classified as a JavaBean (which you’ll learn about later in the chapter).
At
first, we will just focus on the main event of interest for the components being
used. In the case of a JButton, this “event of interest” is
that the button is pressed. To register your interest in when a button is
pressed, you call the JButton’s addActionListener( )
method. This method expects an argument that is an object that implements the
ActionListener interface, which contains a single method called
actionPerformed( ). So all you have to do to attach code to a
JButton is to implement the ActionListener interface in a class,
and register an object of that class with the JButton via
addActionListener( ). The method will be called when the button is
pressed (this is normally referred to as a
callback).
But what should the result of pressing
that button be? We’d like to see something change on the screen, so a new
Swing component will be introduced: the
JTextField. This is a place where text can be
typed, or in this case modified by the program. Although there are a number of
ways to create a JTextField, the simplest is just to tell the constructor
how wide you want that field to be. Once the JTextField is placed on the
form, you can modify its contents by using the setText( ) method
(there are many other methods in JTextField, but you must look these up
in the HTML documentation for the JDK from java.sun.com). Here is what it
looks like:
//: c13:Button2.java // Responding to 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"); JTextField txt = new JTextField(20); class BL implements ActionListener { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.setText(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); cp.add(txt); } public static void main(String[] args) { Console.run(new Button2(), 200, 50); } } ///:~
Creating a JTextField and placing
it on the canvas takes the same steps as for JButtons, or for any Swing
component. The difference in the above program is in the creation of the
aforementioned ActionListener class BL. The argument to
actionPerformed( ) is of type ActionEvent, which contains all
the information about the event and where it came from. In this case, I wanted
to describe the button that was pressed: getSource( ) produces the
object where the event originated, and I assumed that is a JButton.
getText( ) returns the text that’s on the button, and this is
placed in the JTextField to prove that the code was actually called when
the button was pressed.
In init( ),
addActionListener( ) is used to register the BL object with
both the buttons.
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"); JTextField txt = new JTextField(20); ActionListener al = new ActionListener() { public void actionPerformed(ActionEvent e){ String name = ((JButton)e.getSource()).getText(); txt.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(txt); } public static void main(String[] args) { Console.run(new Button2b(), 200, 50); } } ///:~
The approach of using an anonymous inner
class will be preferred (when possible) for the examples in this
book.
A
JTextArea is like a JTextField except that
it can have multiple lines and has more functionality. A particularly useful
method is append( ); with this you can easily pour output into the
JTextArea, thus making a Swing program an improvement (since you can
scroll backwards) over what has been accomplished thus far using command-line
programs that print to standard output. As an example, the following program
fills a JTextArea with the output from the geography generator in
Chapter 9:
//: c13:TextArea.java // Using the JTextArea control. // <applet code=TextArea width=475 height=425> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import java.util.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class TextArea extends JApplet { JButton b = new JButton("Add Data"), c = new JButton("Clear Data"); JTextArea t = new JTextArea(20, 40); Map m = new HashMap(); public void init() { // Use up all the data: Collections2.fill(m, Collections2.geography, CountryCapitals.pairs.length); b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(Iterator it= m.entrySet().iterator(); it.hasNext();){ Map.Entry me = (Map.Entry)(it.next()); t.append(me.getKey() + ": " + me.getValue() + "\n"); } } }); c.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ t.setText(""); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(new JScrollPane(t)); cp.add(b); cp.add(c); } public static void main(String[] args) { Console.run(new TextArea(), 475, 425); } } ///:~
In init( ), the Map is
filled with all the countries and their capitals. Note that for both buttons the
ActionListener is created and added without
defining an intermediate variable, since you never need to refer to that
listener again during the program. The “Add Data” button formats and
appends all the data, while the “Clear Data” button uses
setText( ) to remove all the text from the
JTextArea.
As the JTextArea is added to the
applet, it is wrapped in a
JScrollPane, to control
scrolling when too much text is placed on the screen. That’s all you must
do in order to produce full scrolling capabilities. Having tried to figure out
how to do the equivalent in some other GUI programming environments, I am very
impressed with the simplicity and good design of components like
JScrollPane.
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 not by absolute positioning but 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 the
window dimension is changed, the size, shape, and placement of the components
can change in response.
JApplet,
JFrame JWindow, and JDialog can all produce a Container
with getContentPane( ) which can contain and display
Components. In Container, there’s a method called
setLayout( ) that allows you to choose a
different layout manager. Other classes, such as
JPanel, contain and display components directly
and so you also set the layout manager directly, without using the content
pane.
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
these examples are just intended to show how the buttons are laid
out.
So far, all the applets that have been
created seem to have arranged their components using some mysterious internal
logic. That’s because the applet uses a default layout scheme: the
BorderLayout. Without any other instruction, this
takes whatever you add( ) to it and places it in the center,
stretching the object all the way out to the edges.
However, there’s more to the
BorderLayout. 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 can use the overloaded add( ) method that
takes a constant value as its first argument. This value can be any of the
following:
BorderLayout. NORTH
(top)
BorderLayout. SOUTH
(bottom)
BorderLayout. EAST
(right)
BorderLayout. WEST
(left)
BorderLayout.CENTER (fill the
middle, up to the other components or to the edges)
If you don’t specify an area to
place the object, it defaults to CENTER.
Here’s a simple example. The
default layout is used, since JApplet defaults to
BorderLayout:
//: 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.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 in both dimensions to occupy the
middle.
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.
Here’s an example that sets the
layout manager to FlowLayout and then places buttons on the form.
You’ll notice that with FlowLayout the components take on their
“natural” size. A JButton, 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, because a JLabel will be the size of
its string, attempting to right-justify its text yields an unchanged display
when using
FlowLayout.
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 because no “balancing” goes
on with a
GridLayout.
The
GridBagLayout provides you with tremendous
control in deciding exactly how the regions of your window will lay themselves
out and reformat themselves when the window is resized. However, it’s also
the most complicated layout manager, and quite difficult to understand. It is
intended primarily for automatic code generation by a GUI builder (good GUI
builders will use GridBagLayout instead of absolute placement). If your
design is so complicated that you feel you need to use GridBagLayout,
then you should be using a GUI builder tool to generate that design. If you feel
you must know the intricate details, I’ll refer you to Core Java 2
by Horstmann & Cornell (Prentice-Hall, 1999), or a dedicated Swing book, as
a starting point.
Some
GUI builders use this approach extensively, but this is usually not the best way
to generate code. More useful GUI builders will use GridBagLayout
instead.
Because people had so much trouble
understanding and working with GridBagLayout, Swing also includes the
BoxLayout, which gives you many of the benefits of GridBagLayout
without the complexity, so you can often use it when you need to do hand-coded
layouts (again, if your design becomes too complex, use a GUI builder that
generates GridBagLayouts for you). BoxLayout allows you to control
the placement of components either vertically or horizontally, and to control
the space between the components using something called
“struts and glue.”
First, let’s see how to use the BoxLayout directly, in the same way
that the other layout managers have been demonstrated:
//: 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); } } ///:~
Once you have a Box, you pass it
as a second argument when adding components to the content
pane.
Struts add space between components,
measured in pixels. To use a strut, you simply add it in between the addition of
the components that you want spaced apart:
//: 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); } } ///:~
Struts separate components by a fixed
amount, but glue is the opposite: it separates components by as much as
possible. Thus it’s more of a “spring” than “glue”
(and the design on which this was based was called “springs and
struts” so the choice of the term is a bit mysterious).
//: 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); } } ///:~
A strut works in one direction, but a
rigid area fixes the spacing between components in both
directions:
//: 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.
Swing is powerful; it can get a lot done
with a few lines of code. The examples shown in this book are reasonably simple,
and for learning purposes it makes sense to write them by hand. You can actually
accomplish quite a bit by combining simple layouts. At some point, however, it
stops making sense to hand-code GUI forms—it becomes too complicated and
is not a good use of your programming time. The Java and Swing designers
oriented the language and libraries to support GUI building tools, which have
been created for the express purpose of making your programming experience
easier. As long as you understand what’s going on with layouts and how to
deal with the events (described next), it’s not too important that you
actually know the details of how to lay out components by hand—let the
appropriate tool do that for you (Java is, after all, designed to increase
programmer productivity).
In the Swing 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. Since you typically
use Swing components as they are, but need to write code that is called when the
components receive an event, this is an excellent example of the separation of
interface and implementation.
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 an 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 discover your mistake at
compile-time. You’ll see later in the chapter that JavaBeans also use the
names of the “addListener” methods to determine what events a Bean
can handle.
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.
All the examples so far in this chapter
have been using the Swing event model, but the remainder of this section will
fill out the details of that model.
All Swing components 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:
addMyListener(MyListener m). The following table includes the basic
associated events, listeners, and methods, along with the basic components that
support those particular events by providing the
addXXXListener( ) and
removeXXXListener( ) methods. You should
keep in mind that the event model is designed to be extensible, so you may
encounter other events and
listener types that are not covered in this table.
Event, listener interface and add- and
remove-methods |
Components supporting this
event |
---|---|
ActionEvent |
JButton, JList, JTextField, JMenuItem
and its derivatives including JCheckBoxMenuItem, JMenu, and
JpopupMenu. |
AdjustmentEvent |
JScrollbar |
ComponentEvent |
*Component and its derivatives,
including JButton, JCanvas, JCheckBox, JComboBox, Container, JPanel, JApplet,
JScrollPane, Window, JDialog, JFileDialog, JFrame, JLabel, JList, JScrollbar,
JTextArea, and JTextField. |
ContainerEvent |
Container and its derivatives,
including JPanel, JApplet, JScrollPane, Window, JDialog, JFileDialog,
and JFrame. |
FocusEvent |
Component and
derivatives*. |
KeyEvent |
Component and
derivatives*. |
MouseEvent (for both clicks and
motion) |
Component and derivatives*.
|
MouseEvent[69]
(for both clicks and
motion) |
Component and
derivatives*. |
WindowEvent |
Window and its derivatives,
including JDialog, JFileDialog, and JFrame. |
ItemEvent |
JCheckBox, JCheckBoxMenuItem,
JComboBox, JList, and anything that implements the ItemSelectable
interface. |
TextEvent |
Anything derived from JTextComponent,
including JTextArea and JTextField. |
You can see that each type of component
supports only certain types of events. It turns out to be rather difficult to
look up all the events supported by each component. A simpler approach is to
modify the ShowMethodsClean.java program from Chapter 12 so that it
displays all the event listeners supported by any Swing component that you
enter.
Chapter 12
introduced 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 that 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 methods 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 the tool in that chapter was developed as a command-line
application. Here is the more useful GUI version, specialized to look for the
“addListener” methods in Swing components:
//: c13:ShowAddListeners.java // Display the "addXXXListener" methods of any // Swing class. // <applet code = ShowAddListeners // width=500 height=400></applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import java.lang.reflect.*; import java.io.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class ShowAddListeners extends JApplet { Class cl; Method[] m; Constructor[] ctor; String[] n = new String[0]; JTextField name = new JTextField(25); 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("javax.swing." + nm); } catch (ClassNotFoundException ex) { results.setText("No match"); return; } m = cl.getMethods(); // Convert to an array of Strings: n = new String[m.length]; for(int i = 0; i < m.length; i++) n[i] = m[i].toString(); reDisplay(); } } void reDisplay() { // Create the result set: String[] rs = new String[n.length]; int j = 0; for (int i = 0; i < n.length; i++) if(n[i].indexOf("add") != -1 && n[i].indexOf("Listener") != -1) rs[j++] = n[i].substring(n[i].indexOf("add")); results.setText(""); for (int i = 0; i < j; i++) results.append( StripQualifiers.strip(rs[i]) + "\n"); } public void init() { name.addActionListener(new NameL()); JPanel top = new JPanel(); top.add(new JLabel( "Swing class name (press ENTER):")); top.add(name); Container cp = getContentPane(); cp.add(BorderLayout.NORTH, top); cp.add(new JScrollPane(results)); } public static void main(String[] args) { Console.run(new ShowAddListeners(), 500,400); } } ///:~
The StripQualifiers class defined
in Chapter 12 is reused here by importing the com.bruceeckel.util
library.
The GUI contains a JTextField
name in which you can enter the Swing class name you want to look up. The
results are displayed in a JTextArea.
You’ll notice that there are no
buttons or other components by which to indicate that you want the search to
begin. That’s because the JTextField is monitored by an
ActionListener. Whenever you make a change and press ENTER, the list is
immediately updated. If the text isn’t empty, it is used inside
Class.forName( ) to
try to look up the class. If the name is incorrect, Class.forName( )
will fail, which means that it throws an exception. This is trapped and the
JTextArea is set to “No match”. But if you type in a correct
name (capitalization counts), Class.forName( ) is successful and
getMethods( ) will return an array of Method objects. Each of
the objects in the array is turned into a String via
toString( ) (this produces the complete method signature) and added
to n, a String array. The array n is a member of class
ShowAddListeners and is used in updating the display whenever
reDisplay( ) is called.
reDisplay( ) creates an array
of String called rs (for “result set”). The result set
is conditionally copied from the Strings in n that contain
“add” and “Listener.” indexOf( ) and
substring( ) are then used to remove the qualifiers like
public, static, etc. Finally, StripQualifiers.strip( )
removes the extra name qualifiers.
This program is a convenient way to
investigate the capabilities of a Swing component. 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) |
This is not an exhaustive listing, partly
because the event model allows you to create your own event types and associated
listeners. Thus, you’ll regularly come across libraries that have invented
their own events, and the knowledge gained in this chapter will allow you to
figure out how to use these events.
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 can be less pleasant
to use. For example, something you must always do when creating an application
is provide a WindowListener to the JFrame so that when you get the
windowClosing( ) event you can call System.exit( ) 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, some (but not all)
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 empty methods for each of the interface methods. 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 (remember that this has been wrapped inside the Console class
in com.bruceeckel.swing):
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. Despite the inconvenience, an
interface will guarantee that the methods are properly
implemented.
To prove to yourself that these events
are in fact being fired, and as an interesting experiment, it’s worth
creating an applet that tracks extra behavior in a JButton (other than
just whether it’s pressed or not). This example also shows you how to
inherit your own button object because that’s what is used as the target
of all the events of interest. To do so, you can just inherit
from
JButton[70].
The MyButton class is an inner
class of TrackEvent, so 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
limited solution, since myButton can be used only in conjunction with
TrackEvent. This kind of code is sometimes called “highly
coupled:”
//: 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.*; 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(Color.blue, "test1"), b2 = new MyButton(Color.red, "test2"); class MyButton extends JButton { 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 MyButton(Color color, String label) { super(label); setBackground(color); addFocusListener(fl); addKeyListener(kl); addMouseListener(ml); addMouseMotionListener(mml); } } 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); } } ///:~
In the MyButton constructor, the
button’s color is set with a call to SetBackground( ). The
listeners are all installed with simple method calls.
The TrackEvent class contains a
HashMap to hold the strings representing the type
of event and JTextFields 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.
When report( ) is called it
is given the name of the event and the parameter string from the event. It uses
the HashMap h in the outer class to look up the actual JTextField
associated with that event name, and then places the parameter string into
that field.
This example is fun to play with since
you can really see what’s going on with the events in your
program.
Now that you understand layout managers
and the event model, you’re ready to see how Swing components can be used.
This section is a non-exhaustive tour of the Swing components and features that
you’ll probably use most of the time. Each example is intended to be
reasonably small so that you can easily lift the code and use it in your own
programs.
Keep in mind:
Swing includes a number of different
types of buttons. All buttons, check boxes, 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); } } ///:~
This begins with the
BasicArrowButton from
javax.swing.plaf.basic, then continues with the various specific types of
buttons. 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.” 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. The second
argument to makeBPanel( ) is an array of String. For each
String, a button of the class represented by the first argument is added
to the JPanel:
//: 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.
Note that path information is hard-coded
into this example; you will need to change the path to correspond to the
location of the image files.
//: 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 nice animated
feel.
The previous example added a “tool
tip” to the button. 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.
This example shows the extra behavior
that JTextFields are capable of:
//: c13:TextFields.java // Text fields and Java events. // <applet code=TextFields 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 TextFields 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 TextFields(), 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 JTextField t3 is included as a
place to report when the action listener for the JTextField t1 is
fired. You’ll see that the action listener for a JTextField is
fired only when you press the “enter” key.
The JTextField t1 has several
listeners attached to it. The T1 listener is a DocumentListener
that responds to any change in the “document” (the contents of
the JTextField, in this case). It automatically copies all text
from t1 into t2. In addition, t1’s document is set to
a derived class of PlainDocument, called UpperCaseDocument, which
forces all characters to upper case. It automatically detects backspaces and
perform the deletion, adjusting the caret and handling everything as you would
expect.
JComponent 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); } } ///:~
You can also create your own borders and
put them inside buttons, labels, etc.—anything derived from
JComponent.
Most of the time you’ll just want
to let a JScrollPane do it’s job, but you can also control which
scroll bars are allowed—vertical, horizontal, both, or
neither:
//: c13:JScrollPanes.java // Controlling the scrollbars in a JScrollPane. // <applet code=JScrollPanes 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 JScrollPanes 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 JScrollPanes(), 300, 725); } } ///:~
Using different arguments in the
JScrollPane constructor controls the scrollbars that are available. This
example also dresses things up a bit using
borders.
The
JTextPane control provides a great deal of
support for editing, without much effort. The following example makes very
simple use of this, ignoring the bulk of the functionality of the
class:
//: c13:TextPane.java // The JTextPane control is a little editor. // <applet code=TextPane width=475 height=425> // </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; import com.bruceeckel.util.*; public class TextPane extends JApplet { JButton b = new JButton("Add Text"); JTextPane tp = new JTextPane(); static Generator sg = new Arrays2.RandStringGenerator(7); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ for(int i = 1; i < 10; i++) tp.setText(tp.getText() + sg.next() + "\n"); } }); Container cp = getContentPane(); cp.add(new JScrollPane(tp)); cp.add(BorderLayout.SOUTH, b); } public static void main(String[] args) { Console.run(new TextPane(), 475, 425); } } ///:~
The button just adds randomly-generated
text. The intent of the JTextPane is to allow text to be edited in place,
so you will see that there is no append( ) method. In this case
(admittedly, a poor use of the capabilities of JTextPane), the text must
be captured, modified, and placed back into the pane using
setText( ).
As mentioned before, the default layout
behavior of an applet is to use the BorderLayout. If you add something
the pane without specifying any details, it just fills the center of the pane
out to the edges. However, if you specify one of the surrounding regions (NORTH,
SOUTH, EAST or WEST) as is done here, the component will fit itself into that
region—in this case, the button will nest down at the bottom of the
screen.
Notice the built-in features of
JTextPane, such as automatic line wrapping. There are lots of other
features that you can look up using the JDK
documentation.
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
JCheckBox 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 JCheckBox has been
created.
Whenever a JCheckBox is set or
cleared, an event occurs, which you can capture the same way you do a button, by
using an ActionListener. The following example uses a JTextArea to
enumerate all the check boxes that have been checked:
//: c13:CheckBoxes.java // Using JCheckBoxes. // <applet code=CheckBoxes width=200 height=200> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class CheckBoxes 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 CheckBoxes(), 200, 200); } } ///:~
The trace( ) method sends the
name of the selected JCheckBox and its current state to the
JTextArea using append( ), 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.
All you need to do to set up an
associated group of JRadioButtons is to add them
to a ButtonGroup (you can have any number of
ButtonGroups on a form). One of the buttons can optionally have its
starting state set to true (using the second argument in the
constructor). 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:RadioButtons.java // Using JRadioButtons. // <applet code=RadioButtons // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class RadioButtons extends JApplet { JTextField t = new JTextField(15); ButtonGroup g = new ButtonGroup(); JRadioButton rb1 = new JRadioButton("one", false), rb2 = new JRadioButton("two", false), 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 RadioButtons(), 200, 100); } } ///:~
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. Thus it is an alternative to using a
JLabel.
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 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
JComboBox box is not like the combo box in Windows, which lets you select
from a list or type in your own selection. With a JComboBox box
you choose one and only one element from the list. In the following example, the
JComboBox box starts with a certain number of entries and then new
entries are added to the box when a button is pressed.
//: c13:ComboBoxes.java // Using drop-down lists. // <applet code=ComboBoxes // width=200 height=100> </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class ComboBoxes 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 ComboBoxes(), 200, 100); } } ///:~
The JTextField displays the
“selected index,” which is the sequence number of the currently
selected element, as well as the label on the radio
button.
List boxes are significantly different
from JComboBox boxes, and not just in appearance. While a
JComboBox box drops down when you activate it, a
JList occupies some fixed number of lines on a
screen all the time and doesn’t change. If you want to see the items in a
list, you simply call getSelectedValues( ),
which produces an array of String of the items that have been
selected.
A JList allows multiple selection:
if you control-click on more than one item the original item stays highlighted
and you can select as many as you want. If you select an item, then shift-click
on another item, all the items in the span between the two are selected. To
remove an item from a group you can control-click it.
//: c13:List.java // <applet code=List width=250 // height=375> </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 List 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 List(), 250, 375); } } ///:~
When you press the button it adds items
to the top of the list (because addItem( )’s
second argument is 0).
You can see that borders have also been
added to the lists.
If you just want to put an array of
Strings into a JList, there’s a much simpler solution: you
pass the array to the JList constructor, and it builds the list
automatically. The only reason for using the “list model” in the
above example is so that the list could be manipulated during the execution of
the program.
JLists do not automatically
provide direct support for scrolling. Of course, all you need to do is wrap the
JList in a JScrollPane and all the details
are automatically managed for
you.
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(); JTextField txt = new JTextField(20); 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) { txt.setText("Tab selected: " + tabs.getSelectedIndex()); } }); Container cp = getContentPane(); cp.add(BorderLayout.SOUTH, txt); cp.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 in applet
programming the use of pop-up dialogs is discouraged by automatically adding a
little warning to any dialog that pops up out of an applet.
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( ). The following example shows a subset of the
message boxes available with JOptionPane:
//: c13:MessageBoxes.java // Demonstrates JoptionPane. // <applet code=MessageBoxes // width=200 height=200> </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") }; JTextField txt = new JTextField(20); 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) txt.setText( "Color Selected: " + options[sel]); } else if(id.equals("Input")) { String val = JOptionPane.showInputDialog( "How many fingers do you see?"); txt.setText(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) txt.setText( 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]); } cp.add(txt); } public static void main(String[] args) { Console.run(new MessageBoxes(), 200, 200); } } ///:~
To be able to write a single
ActionListener, I’ve used the somewhat risky approach of checking
the String labels on the buttons. The problem with this is that
it’s easy to get the label a little bit wrong, typically in
capitalization, and this bug can be hard to spot.
Note that showOptionDialog( )
and showInputDialog( ) provide return objects that contain the value
entered by the user.
Each component capable of holding a menu,
including JApplet, JFrame, JDialog, and their descendants,
has a setJMenuBar( ) method that accepts a JMenuBar (you can
have only one JMenuBar on a particular component). You add JMenus
to the JMenuBar, and JMenuItems to the JMenus. Each
JMenuItem can have an ActionListener attached to it, to be fired
when 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 is a very simple menu example: //: 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() { for(int i = 0; i < items.length; i++) { items[i].addActionListener(al); menus[i%3].add(items[i]); } JMenuBar mb = new JMenuBar(); for(int i = 0; i < menus.length; i++) mb.add(menus[i]); setJMenuBar(mb); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(t); } public static void main(String[] args) { Console.run(new SimpleMenus(), 200, 75); } } ///:~
The use of the modulus operator in’
‘i%3’ distributes the menu items among the three
JMenus. Each JMenuItem must have an ActionListener attached
to it; here, the same ActionListener is used everywhere but you’ll
usually need an individual one for each JMenuItem.
JMenuItem
inherits AbstractButton, so it has some button-like behaviors. By
itself, it provides an item that can be placed on a drop-down menu. There are
also three types inherited from JMenuItem: JMenu to hold other
JMenuItems (so you can have cascading menus), JCheckBoxMenuItem,
which produces a checkmark to indicate whether that menu item is selected, and
JRadioButtonMenuItem which contains a radio button.
As a more sophisticated example, here are
the ice cream flavors again, used to create menus. This example also shows
cascading menus, keyboard mnemonics, JCheckBoxMenuItems, and the
way you can dynamically change menus:
//: c13:Menus.java // Submenus, checkbox menu items, swapping menus, // mnemonics (shortcuts) and action commands. // <applet code=Menus width=300 // height=100> </applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class Menus 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"), }; // A second menu bar to swap to: JMenuBar mb2 = new JMenuBar(); JMenu fooBar = new JMenu("fooBar"); JMenuItem[] other = { // Adding a menu shortcut (mnemonic) is very // simple, but only JMenuItems can have them // in their constructors: new JMenuItem("Foo", KeyEvent.VK_F), new JMenuItem("Bar", KeyEvent.VK_A), // No shortcut: 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 } } 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!"); } } } 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].setMnemonic(KeyEvent.VK_G); safety[0].addItemListener(cmil); safety[1].setActionCommand("Hide"); safety[0].setMnemonic(KeyEvent.VK_H); safety[1].addItemListener(cmil); 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]); s.setMnemonic(KeyEvent.VK_A); f.add(s); f.setMnemonic(KeyEvent.VK_F); for(int i = 0; i < file.length; i++) { file[i].addActionListener(fl); 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()); b.setMnemonic(KeyEvent.VK_S); cp.add(b, BorderLayout.NORTH); for(int i = 0; i < other.length; i++) fooBar.add(other[i]); fooBar.setMnemonic(KeyEvent.VK_B); mb2.add(fooBar); } public static void main(String[] args) { Console.run(new Menus(), 300, 100); } } ///:~
In this program I placed the menu items
into arrays and then stepped through each array calling add( ) for
each JMenuItem. This makes adding or subtracting a menu item somewhat
less tedious.
This program creates not one but two
JMenuBars to demonstrate that menu bars can be actively swapped while the
program is running. You can see how a JMenuBar is made up of
JMenus, and each JMenu is made up of JMenuItems,
JCheckBoxMenuItems, or even other JMenus (which produce submenus).
When a JMenuBar is assembled it can be installed into the current program
with the setJMenuBar( ) method. Note that when the button is
pressed, it checks to see which menu is currently installed by calling
getJMenuBar( ), then it 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 source of programming errors.
The checking and un-checking of the menu
items is taken care of automatically. The code handling the
JCheckBoxMenuItems shows two different ways to determine what was
checked: string matching (which, as mentioned above, isn’t a very safe
approach although you’ll see it used) and matching on the event target
object. As shown, the getState( ) method can
be used to reveal the state. You can also change the state of a
JCheckBoxMenuItem with setState( ).
The events for menus are a bit
inconsistent and can lead to confusion:
JMenuItems use ActionListeners, but
JCheckboxMenuItems use ItemListeners. The
JMenu objects can also support
ActionListeners, but that’s not usually helpful. In general,
you’ll attach listeners to each JMenuItem,
JCheckBoxMenuItem, or JRadioButtonMenuItem, but the example shows
ItemListeners and ActionListeners attached to the various menu
components.
Swing
supports mnemonics, or “keyboard shortcuts,” so you can select
anything derived from AbstractButton (button, menu item, etc.) using the
keyboard instead of the mouse. These are quite simple: for JMenuItem you
can use the overloaded constructor that takes as a second argument the
identifier for the key. However, most AbstractButtons do not have
constructors like this so the more general way to solve the problem is to use
the setMnemonic( ) method. The example above
adds mnemonics to the button and some of the menu items; shortcut indicators
automatically appear on the components.
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 change the code which would 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.
The bulk of the work happens in the
listeners. BL performs the JMenuBar
swapping. In ML, the “figure out who rang” approach is taken
by getting the source of the ActionEvent and
casting it to a JMenuItem, then getting the
action command string to pass it through a cascaded if
statement.
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.
You can see that menu code quickly gets
long-winded and messy. This is another case where the use of a GUI builder is
the appropriate solution. A good tool will also handle the maintenance of the
menus.
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
that you want to 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.
In a good GUI framework, drawing should
be reasonably easy, and it is, in the Swing library. The problem with any
drawing example is that the calculations that determine where things go are
typically a lot more complicated that the calls to the drawing routines, and
these calculations are often mixed together with the drawing calls so it can
seem that the interface is more complicated than it actually
is.
For simplicity, consider the problem of
representing data on the screen—here, the data will be provided by the
builtin Math.sin( ) method which is a mathematical sine function. To
make things a little more interesting, and to further demonstrate how easy it is
to use Swing components, a slider will be placed at the bottom of the form to
dynamically control the number of sine wave cycles that are displayed. In
addition, if you resize the window, you’ll see that the sine wave refits
itself to the new window size.
Although any
JComponent may be painted and thus used as a
canvas, if you just want a straightforward drawing surface you will typically
inherit from a JPanel. The only method you need
to override is paintComponent( ), which is
called whenever that component must be repainted (you normally don’t need
to worry about this, as the decision is managed by Swing). When it is called,
Swing passes a Graphics object to the method, and
you can then use this object to draw or paint on the surface.
In the following example, all the
intelligence concerning painting is in the SineDraw class; the
SineWave class simply configures the program and the slider control.
Inside SineDraw, the setCycles( ) method provides a hook to
allow another object—the slider control, in this case—to control the
number of cycles.
//: c13:SineWave.java // Drawing with Swing, using a JSlider. // <applet code=SineWave // width=700 height=400></applet> import javax.swing.*; import javax.swing.event.*; import java.awt.*; import com.bruceeckel.swing.*; class SineDraw extends JPanel { static final int SCALEFACTOR = 200; int cycles; int points; double[] sines; int[] pts; SineDraw() { setCycles(5); } public void setCycles(int newCycles) { cycles = newCycles; points = SCALEFACTOR * cycles * 2; sines = new double[points]; pts = new int[points]; for(int i = 0; i < points; i++) { double radians = (Math.PI/SCALEFACTOR) * i; sines[i] = Math.sin(radians); } repaint(); } public void paintComponent(Graphics g) { super.paintComponent(g); int maxWidth = getWidth(); double hstep = (double)maxWidth/(double)points; int maxHeight = getHeight(); for(int i = 0; i < points; i++) pts[i] = (int)(sines[i] * maxHeight/2 * .95 + maxHeight/2); g.setColor(Color.red); for(int i = 1; i < points; i++) { int x1 = (int)((i - 1) * hstep); int x2 = (int)(i * hstep); int y1 = pts[i-1]; int y2 = pts[i]; g.drawLine(x1, y1, x2, y2); } } } public class SineWave extends JApplet { SineDraw sines = new SineDraw(); JSlider cycles = new JSlider(1, 30, 5); public void init() { Container cp = getContentPane(); cp.add(sines); cycles.addChangeListener(new ChangeListener(){ public void stateChanged(ChangeEvent e) { sines.setCycles( ((JSlider)e.getSource()).getValue()); } }); cp.add(BorderLayout.SOUTH, cycles); } public static void main(String[] args) { Console.run(new SineWave(), 700, 400); } } ///:~
All of the data members and arrays are
used in the calculation of the sine wave points: cycles indicates the
number of complete sine waves desired, points contains the total number
of points that will be graphed, sines contains the sine function values,
and pts contains the y-coordinates of the points that will be drawn on
the JPanel. The setCycles( ) method creates the arrays
according to the number of points needed and fills the sines array with
numbers. By calling repaint( ) , setCycles( ) forces
paintComponent( ) to be called so the rest of the calculation and
redraw will take place.
The first thing you must do when you
override paintComponent( ) is to call the base-class version of the
method. Then you are free to do whatever you like; normally, this means using
the Graphics methods that you can find in the documentation for
java.awt.Graphics (in the HTML documentation from java.sun.com) to
draw and paint pixels onto the JPanel. Here, you can see that almost all
the code is involved in performing the calculations; the only two method calls
that actually manipulate the screen are setColor( ) and
drawLine( ). You will probably have a similar experience when
creating your own program that displays graphical data—you’ll spend
most of your time figuring out what it is you want to draw, but the actual
drawing process will be quite simple.
When I created this program, the bulk of
my time was spent in getting the sine wave to display. Once I did that, I
thought it would be nice to be able to dynamically change the number of cycles.
My programming experiences when trying to do such things in other languages made
me a bit reluctant to try this, but it turned out to be the easiest part of the
project. I created a JSlider (the arguments are the left-most value of
the JSlider, the right-most value, and the starting value, respectively,
but there are other constructors as well) and dropped it into the
JApplet. Then I looked at the HTML documentation and noticed that the
only listener was the addChangeListener which was
triggered whenever the slider was changed enough for it to produce a different
value. The only method for this was the obviously-named
stateChanged( ), which provided a
ChangeEvent object so that I could look backward to the source of the
change and find the new value. By calling the sines object’s
setCycles( ), the new value was incorporated and the JPanel
redrawn.
In general, you will find that most of
your Swing problems can be solved by following a similar process, and
you’ll find that it’s generally quite simple, even if you
haven’t used a particular component before.
If your problem is more complex, there
are other more sophisticated alternatives for drawing including third-party
JavaBeans components and the Java 2D API. These solutions are beyond the scope
of this book but you should look them up if your drawing code becomes too
onerous.
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 less frequently used in
applets.
To create a dialog box, you inherit from
JDialog, which is just another kind of Window, like a
JFrame. A JDialog has a layout manager (which defaults to
BorderLayout) and you add event listeners to deal with events. One
significant difference when windowClosing( )
is called is that you don’t want to shut down the application. Instead,
you release the resources used by the dialog’s window by calling
dispose( ). Here’s a very simple
example:
//: c13:Dialogs.java // Creating and using Dialog Boxes. // <applet code=Dialogs width=125 height=75> // </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){ dispose(); // Closes the dialog } }); cp.add(ok); setSize(150,125); } } public class Dialogs 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(); } }); getContentPane().add(b1); } public static void main(String[] args) { Console.run(new Dialogs(), 125, 75); } } ///:~
Once the JDialog is created, the
show( ) method must be called to display and
activate it. For the dialog to close, it must call
dispose( ).
You’ll see 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 following example is more complex;
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.*; public class TicTacToe 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); // JDK 1.3 close dialog: //#setDefaultCloseOperation( //# DISPOSE_ON_CLOSE); // JDK 1.2 close dialog: 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 TicTacToe(), 200, 100); } } ///:~
Because statics can only be at the
outer level of the class, inner classes cannot have static data or
static inner classes.
The
paintComponent( ) method draws the square around the panel, and the
“x” or the “o.” This is full of tedious calculations,
but it’s straightforward.
A mouse click is captured by the
MouseListener, which first checks to see if the panel 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 ToeButton. Via the inner
class mechanism, the ToeButton 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 state
change, the ToeButton 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.
TicTacToe sets up the whole
application by creating the JTextFields (for inputting the rows and
columns of the button grid) and the “go” button with its
ActionListener. When the button is pressed, the data in the
JTextFields must be fetched, and, since they are in String form,
turned into ints using the static
Integer.parseInt( )
method.
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 action listeners for the two different button clicks:
//: c13:FileChooserTest.java // Demonstration of File dialog boxes. // <applet code= FileChooserTest // width=250 height=110></applet> 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 void init() { 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. Also, this will compile and run as an Applet/application,
but when run as an applet the behavior is a bit odd (in
my case, it tried to access the floppy drive but wouldn’t do anything
else). Of course, an applet shouldn’t provide any way to access the
local disk, for security reasons.
For an “open file” dialog,
you call showOpenDialog( ), and for a “save file” dialog
you call showSaveDialog( ). These commands don’t return until
the dialog is closed. The JFileChooser object still exists, so you can
read data from it. The methods getSelectedFile( ) and
getCurrentDirectory( ) are two ways you can interrogate the results
of the operation. If these return null it means the user canceled out of
the dialog.
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,
//: c13:HTMLButton.java // Putting HTML text on Swing components. // <applet code=HTMLButton width=200 height=500> // </applet> import javax.swing.*; import java.awt.event.*; import java.awt.*; import com.bruceeckel.swing.*; public class HTMLButton extends JApplet { JButton b = new JButton("<html><b><font size=+2>" + "<center>Hello!<br><i>Press me now!"); public void init() { b.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e){ getContentPane().add(new JLabel("<html>"+ "<i><font size=+4>Kapow!")); // Force a re-layout to // include the new label: validate(); } }); Container cp = getContentPane(); cp.setLayout(new FlowLayout()); cp.add(b); } public static void main(String[] args) { Console.run(new HTMLButton(), 200, 500); } } ///:~
You must start the text with
‘<html>’, and then you can use normal HTML tags. Note that you
are not forced to include the normal closing tags.
The ActionListener adds a new
JLabel to the form, which also contains HTML text. However, this label is
not added during init( ) so you must call the container’s
validate( ) method in order to force a re-layout of the components
(and thus the display of the new label).
A slider (which
has already been used in the sine wave example) 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 key to hooking the two components
together is in sharing their model, in the line:
pb.setModel(sb.getModel());
Of course, you could also control the two using a listener, but this is more straightforward for simple situations.
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 is used if nothing is selected) and the
model’s insertNodeInto( ) method does
all the work of changing the tree and causing it to be updated.
An example like the one above may 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.
Despite this, it’s a good design and the alternatives are usually much
worse.
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=200></applet> import javax.swing.*; import java.awt.*; import java.awt.event.*; import javax.swing.table.*; import javax.swing.event.*; import com.bruceeckel.swing.*; public class Table extends JApplet { JTextArea txt = new JTextArea(4, 20); // 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){ txt.setText(""); // Clear it for(int i = 0; i < data.length; i++) { for(int j = 0; j < data[0].length; j++) txt.append(data[i][j] + " "); txt.append("\n"); } } } 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 void init() { Container cp = getContentPane(); JTable table = new JTable(new DataModel()); cp.add(new JScrollPane(table)); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new Table(), 350, 200); } } ///:~
DataModel contains an array of
data, but you could also get the data from some other source such as a database.
The constructor adds a TableModelListener which prints the array every
time the table is changed. The rest of the methods follow the Beans naming
convention, and are used by JTable when it wants to present the
information in DataModel. AbstractTableModel provides default
methods for setValueAt( ) and isCellEditable( ) that
prevent changes to the data, so if you want to be able to edit the data, you
must override these methods.
Once you have a TableModel, you
only need to hand it to the JTable constructor. All the details of
displaying, editing and updating will be taken care of for you. This example
also puts the JTable in a
JScrollPane.
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 several different
components look under the chose look and feel:
//: 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!).
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
JTextArea. 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 JTextField or JTextArea 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
something other than a JTextComponent.
//: 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 JTextArea 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.
An important use of the JAR utility 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 solve the problem by
compressing all of your .class files into a single file that is
downloaded by the browser. Now you can create the right design without worrying
about how many .class files it will generate, and the user will get a
much faster download time.
Consider TicTacToe.java. It looks
like a single class, but in fact it contains five inner classes, so that’s
six in all. Once you’ve compiled the program, you package it into a JAR
file with the line:
jar cf TicTacToe.jar *.class
This assumes that the only .class
files in the current directory are the ones from TicTacToe.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. Here is the tag using the old form of
the HTML tag, as an illustration:
<head><title>TicTacToe Example Applet </title></head> <body> <applet code=TicTacToe.class archive=TicTacToe.jar width=200 height=100> </applet> </body>
You’ll need to put it into the new (messy, complicated) form shown earlier in the chapter in order to get it to work."_Toc479507737">
Because GUI programming in Java has been
an evolving technology with some very significant changes between Java 1.0/1.1
and the Swing library in Java 2, there have been some old programming idioms
that have seeped through to examples that you might see given for Swing. In
addition, Swing allows you to program in more and better ways than were allowed
by the old models. In this section, some of these issues will be demonstrated by
introducing and examining some programming
idioms.
One of the benefits of the
Swing event model is
flexibility. 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=250 height=400></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"); JTextArea txt = new JTextArea(); class B implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("A button was pressed\n"); } } class CountListener implements ActionListener { int index; public CountListener(int i) { index = i; } public void actionPerformed(ActionEvent e) { txt.append("Counted Listener "+index+"\n"); } } class B1 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button 1 pressed\n"); ActionListener a = new CountListener(i++); v.add(a); b2.addActionListener(a); } } class B2 implements ActionListener { public void actionPerformed(ActionEvent e) { txt.append("Button2 pressed\n"); int end = v.size() - 1; if(end >= 0) { b2.removeActionListener( (ActionListener)v.get(end)); v.remove(end); } } } public void init() { Container cp = getContentPane(); b1.addActionListener(new B()); b1.addActionListener(new B1()); b2.addActionListener(new B()); b2.addActionListener(new B2()); JPanel p = new JPanel(); p.add(b1); p.add(b2); cp.add(BorderLayout.NORTH, p); cp.add(new JScrollPane(txt)); } public static void main(String[] args) { Console.run(new DynamicEvents(), 250, 400); } } ///:~
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 tie 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"); 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 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); } 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 Swing 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
to the code design process. Now there are even more different ways for people to
write unpleasant code.
Except in extenuating circumstances you
can always use the simplest and clearest approach:
listener classes (typically written as
inner classes) to solve your
event-handling needs. This is the form used in most of the examples in this
chapter.
By following this model you should be
able to reduce the statements in your programs that say: “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.
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. 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 JavaBeans 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 uses reflection to dynamically
interrogate the component and 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 JavaBeans, 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 when looking at older
code vs. newer code: 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 JavaBean. 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 case
change 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 identifier for internal variables (or, indeed, to even
have any internal variables 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 onto 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
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 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 tool, not only to make Beans simpler to
use but also to provide a standard gateway to the creation of more complex
Beans. This tool 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:
//: c13:BeanDumper.java // Introspecting a Bean. // <applet code=BeanDumper width=600 height=500> // </applet> import java.beans.*; import java.lang.reflect.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import com.bruceeckel.swing.*; public class BeanDumper extends JApplet { JTextField query = new JTextField(20); JTextArea results = new JTextArea(); public void prt(String s) { results.append(s + "\n"); } public void dump(Class bean){ results.setText(""); BeanInfo bi = null; try { bi = Introspector.getBeanInfo( bean, java.lang.Object.class); } catch(IntrospectionException ex) { prt("Couldn't introspect " + bean.getName()); return; } PropertyDescriptor[] properties = bi.getPropertyDescriptors(); for(int i = 0; i < properties.length; i++) { Class p = properties[i].getPropertyType(); prt("Property type:\n " + p.getName() + "Property name:\n " + properties[i].getName()); Method readMethod = properties[i].getReadMethod(); if(readMethod != null) prt("Read method:\n " + readMethod); Method writeMethod = properties[i].getWriteMethod(); if(writeMethod != null) prt("Write method:\n " + writeMethod); prt("===================="); } prt("Public methods:"); MethodDescriptor[] methods = bi.getMethodDescriptors(); for(int i = 0; i < methods.length; i++) prt(methods[i].getMethod().toString()); prt("======================"); prt("Event support:"); EventSetDescriptor[] events = bi.getEventSetDescriptors(); for(int i = 0; i < events.length; i++) { prt("Listener type:\n " + events[i].getListenerType().getName()); Method[] lm = events[i].getListenerMethods(); for(int j = 0; j < lm.length; j++) prt("Listener method:\n " + lm[j].getName()); MethodDescriptor[] lmd = events[i].getListenerMethodDescriptors(); for(int j = 0; j < lmd.length; j++) prt("Method descriptor:\n " + lmd[j].getMethod()); Method addListener = events[i].getAddListenerMethod(); prt("Add Listener Method:\n " + addListener); Method removeListener = events[i].getRemoveListenerMethod(); prt("Remove Listener Method:\n " + removeListener); prt("===================="); } } class Dumper implements ActionListener { public void actionPerformed(ActionEvent e) { String name = query.getText(); Class c = null; try { c = Class.forName(name); } catch(ClassNotFoundException ex) { results.setText("Couldn't find " + name); return; } dump(c); } } public void init() { Container cp = getContentPane(); JPanel p = new JPanel(); p.setLayout(new FlowLayout()); p.add(new JLabel("Qualified bean name:")); p.add(query); cp.add(BorderLayout.NORTH, p); cp.add(new JScrollPane(results)); Dumper dmpr = new Dumper(); query.addActionListener(dmpr); query.setText("frogbean.Frog"); // Force evaluation dmpr.actionPerformed( new ActionEvent(dmpr, 0, "")); } public static void main(String[] args) { Console.run(new BeanDumper(), 600, 500); } } ///:~
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.
Upon startup, the program forces the
evaluation of 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 JPanel 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); } } ///:~
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 “JavaBeans revisited”). A unicast event sidesteps the
problem.
When you click 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).
Here is the BangBeanTest class to
allow you to test the bean as either an applet or an
application:
//: c13:BangBeanTest.java // <applet code=BangBeanTest // width=400 height=500></applet> import bangbean.*; import javax.swing.*; import java.awt.*; import java.awt.event.*; import java.util.*; import com.bruceeckel.swing.*; public class BangBeanTest extends JApplet { JTextField txt = new JTextField(20); // During testing, report actions: class BBL implements ActionListener { int count = 0; public void actionPerformed(ActionEvent e){ txt.setText("BangBean action "+ count++); } } public void init() { BangBean bb = new BangBean(); try { bb.addActionListener(new BBL()); } catch(TooManyListenersException e) {} Container cp = getContentPane(); cp.add(bb); cp.add(BorderLayout.SOUTH, txt); } public static void main(String[] args) { Console.run(new BangBeanTest(), 400, 500); } } ///:~
When a Bean is in a development
environment, this class will not be used, but it’s helpful to provide a
rapid testing method for each of your Beans. BangBeanTest places a
BangBean within the applet, attaching a simple ActionListener to
the BangBean to print an event count to the JTextField 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 JPanel, and JPanel is also 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 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 (without the first and last
lines):
//:! :BangBean.mf 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 JavaBeans in their “Beans Development
Kit” (BDK) called the
“beanbox.” (Download
the BDK from http://java.sun.com/beans.) 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
JavaBeans architecture 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.
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 Chapter 14.
You’ll find a section there called “JavaBeans revisited” that
will look at the problem and its solution.
There are a number of books about
JavaBeans; for example, JavaBeans by Elliotte Rusty Harold (IDG,
1998).
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 the native application development tools
available on a particular platform.
When Java 1.1 introduced the new event
model and JavaBeans, 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
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 Microsoft’s 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
JavaBeans. 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 marketplace can generate only
positive results for the productivity of the programmer.
This chapter 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. There’s probably a way to accomplish just about everything you can imagine.
If
you don’t see what you need here, delve into the online documentation from
Sun and search the Web, and if that’s not enough then find a dedicated
Swing book—a good place to start is The JFC Swing Tutorial, by
Walrath & Campione (Addison Wesley, 1999).
[62]
A variation on this is called “the principle of least astonishment,”
which essentially says: “don’t surprise the
user.”
[63]
This is an example of the design pattern called the template
method.
[64]
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.
[65]
This page—in particular, the ‘clsid’ portion—seemed to
work fine with both JDK1.2.2 and JDK1.3 rc-1. However, you may find that you
have to change the tag sometime in the future. Details can be found at
java.sun.com.
[66]
In my opinion. And after you learn about Swing, you won’t want to waste
your time on the earlier stuff.
[67]
As described earlier, “Frame” was already taken by the AWT, so Swing
uses JFrame.
[68]
This will make sense after you’ve read further in this chapter. First,
make the reference JApplet a static member of the class (instead
of a local variable of main( )), and then call
applet.stop( ) and applet.destroy( ) inside
WindowAdapter.windowClosing( ) before you call
System.exit( ).
[69]
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.
[70]
In Java 1.0/1.1 you could not usefully inherit from the button object.
This was only one of numerous fundamental design flaws.