3.21 JTextComponent and HTML Text Display
The most complex component in all of Swing is the
JTextComponent, which is a powerful
editor. It is part of the
javax.swing.text
package and generally is not used directly. Instead, you
typically use one of
its subclasses, such as
JTextField,
JPasswordField,
JTextArea,
or
JEditorPane. The first three of these
components are straightforward. They are for the entry of a
single line of text, secret text such as a password, and simple,
unformatted, multiline text, respectively.
It is the
JEditorPane component that really
makes use of the full power of
JTextComponent.
JEditorPane
supports the display and editing of complex formatted text. In
conjunction with the classes in the
javax.swing.text.html and
javax.swing.text.rtf packages,
JEditorPane can display and edit
HTML and RTF documents.
The ability to display formatted text so easily is a very powerful
feature. For example, the ability to display
HTML documents makes it simple for a Swing application to add
online help based on an HTML version of the
application's user manual. Furthermore, formatted text is a
professional-looking way for an application to
display its output to the user.
Because HTML has become so ubiquitous, we'll focus on the display
of HTML documents with
JEditorPane,
There are several different ways to get a
JEditorPane to display an HTML document. If
the desired document is available on the network, the easiest way
to display it is simply to pass an appropriate
java.net.URL object to
the
setPage()
method of
JEditorPane.
setPage() determines the data type of the
document and, assuming it is an HTML document, loads it and
displays it as such. For example:
editor.setPage(new java.net.URL("https://www.my.com/product/help.htm"));
If the document you want to display is in a local file or is
available from some kind of
InputStream, you
can display it by passing the appropriate stream to the
read() method of
JEditorPane. The second argument to this
method should be
null. For example:
InputStream in = new FileInputStream("help.htm");
editor.read(in, null);
Yet another way to display text in a
JEditorPane is
to pass the text to the
setText()
method. Before you do this, however, you must tell the editor
what type of text to expect:
editor.setContentType("text/html");
editor.setText("<H1>Hello World!</H1>");
Calling
setText() can be particularly useful
when your application generates HTML text on the fly and wants to
use a
JEditorPane to display nicely formatted
output to the user.
Example 3.4 shows
one such use of the
JEditorPane. This example
is an alternative to Example 3.3: it
displays the contents of a directory in tabular form but uses an
HTML table instead of the
JTable
component. As a bonus, this example uses HTML hyperlinks to allow
the user to browse from one directory to the next. (If you
download and run the two examples, however, you'll probably notice
that the
JTable example is significantly
faster, since it does not have to encode the directory contents
into HTML and then parse that HTML into a table.)
Figure 3.9 shows sample output from
this example.
Figure 3.9: The JEditorPane component displaying an HTML table
Example 3.4: Dynamically Generated HTML in JEditorPane
import javax.swing.*;
import javax.swing.event.*;
import java.io.*;
import java.util.Date;
/**
* This class implements a simple directory browser using the HTML
* display capabilities of the JEditorPane component
**/
public class FileTableHTML {
public static void main(String[] args) throws IOException {
// Get the name of the directory to display
String dirname = (args.length>0)?args[0]:System.getProperty("user.home");
// Create something to display it in
final JEditorPane editor = new JEditorPane();
editor.setEditable(false); // we're browsing not editing
editor.setContentType("text/html"); // must specify HTML text
editor.setText(makeHTMLTable(dirname)); // specify the text to display
// Set up the JEditorPane to handle clicks on hyperlinks
editor.addHyperlinkListener(new HyperlinkListener() {
public void hyperlinkUpdate(HyperlinkEvent e) {
// Handle clicks; ignore mouseovers and other link-related events
if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
// Get the HREF of the link and display it.
editor.setText(makeHTMLTable(e.getDescription()));
}
}
});
// Put the JEditorPane in a scrolling window and display it
JFrame frame = new JFrame("FileTableHTML");
frame.getContentPane().add(new JScrollPane(editor));
frame.setSize(650, 500);
frame.setVisible(true);
}
// This method returns an HTML table representing the specified directory
public static String makeHTMLTable(String dirname) {
// Look up the contents of the directory
File dir = new File(dirname);
String[] entries = dir.list();
// Set up an output stream we can print the table to.
// This is easier than concatenating strings all the time.
StringWriter sout = new StringWriter();
PrintWriter out = new PrintWriter(sout);
// Print the directory name as the page title
out.println("<H1>" + dirname + "</H1>");
// Print an "up" link, unless we're already at the root
String parent = dir.getParent();
if ((parent != null) && (parent.length() > 0))
out.println("<A href=\"" + parent + "\">Up to parent directory</A><P>");
// Print out the table
out.print("<TABLE BORDER=2 WIDTH=600><TR>");
out.print("<TH>Name</TH><TH>Size</TH><TH>Modified</TH>");
out.println("<TH>Readable?</TH><TH>Writable?</TH></TR>");
for(int i=0; i < entries.length; i++) {
File f = new File(dir, entries[i]);
out.println("<TR><TD>" +
(f.isDirectory() ?
"<a href=\""+f+"\">" + entries[i] + "</a>" :
entries[i]) +
"</TD><TD>" + f.length() +
"</TD><TD>" + new Date(f.lastModified()) +
"</TD><TD align=center>" + (f.canRead()?"x":" ") +
"</TD><TD align=center>" + (f.canWrite()?"x":" ") +
"</TD></TR>");
}
out.println("</TABLE>");
out.close();
// Get the string of HTML from the StringWriter and return it.
return sout.toString();
}
}
3.22 Pluggable Look-and-Feel
One of the unique features of Swing is its pluggable
look-and-feel (PLAF) architecture, which
allows a Swing application to change its entire appearance
with one or two lines of code. The most common use of this
feature is to give applications a choice between the native
platform look-and-feel and a new platform-independent Java
look-and-feel (also known as the Metal look-and-feel). Swing
is distributed with three look-and-feels: Metal and two
look-and-feels that
mimic the appearance and behavior of the Windows and Motif
(Unix/X) component toolkits. A look-and-feel that mimics the
Macintosh platform is available as a separate download. While the
Metal and Motif look-and-feels can be freely used, the Windows
look-and-feel is restricted for use only on Windows platform - for
copyright reasons, it does not run on any other operating system.
When a Swing application starts up, it reads the system property
swing.defaultlaf to determine the classname of
the default look-and-feel. In most Java installations, this
property is set to the default Java look-and-feel, implemented by
the class
javax.swing.plaf.metal.MetalLookAndFeel.
The end user can override this default by using the
-D switch on the command line when invoking the
Java interpreter. For example, to run a Swing application using
the Motif look-and-feel, a user can type:
% java -Dswing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel app
If the user is using a Windows operating system, he can
start the application using the Windows look-and-feel like this:
% java -Dswing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel app
When you write a Swing application, you can explicitly set the
look-and-feel that the application uses. To do this, simply call
the static
setLookAndFeel() method of the
UIManager class and specify the classname
of the desired look-and-feel implementation. To make this even
easier,
UIManager defines a static method that
returns the classname of the default cross-platform look-and-feel
(i.e., Metal) and another that returns
the classname of the look-and-feel that mimics
the native look-and-feel of the current platform. So, if you want
your application to always look like a native application, you
can simply include this line of code in your application, before
it begins to create any GUI components:
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
Or, if you want to force the application to use the cross-platform
look-and-feel, regardless of installation defaults and user
preferences, you can use this line of code:
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
Note that calling
setLookAndFeel() like this
overrides the value of the
swing.defaultlaf property, if the end user
has set one. Of course, the command-line
syntax for setting that property is quite awkward and may be
beyond the capabilities of many end users. An alternative is to
implement command-line options in your own application that give
the user a choice of look-and-feels. You
might set a native look-and-feel if the user specifies a
-nativelook flag on the command line, for
example.
The easiest time to call the
setLookAndFeel()
method is at application start-up, before any Swing components
have been created. It is also possible to change the look-and-feel of
a running application, however. This means that you can allow
the user to change the current look-and-feel through a preferences
dialog box, if you are so inclined.
When the user selects a new look-and-feel, you
first call
setLookAndFeel() to install the new
look-and-feel, and then you have to notify all of the Swing
components that a new look-and-feel is in effect and ask them to
use it. Fortunately, there is a convenience method to do this.
Your code might look like this:
// Set the new look-and-feel
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName);
// Tell all components from main JFrame on down that LAF has changed
SwingUtilities.updateComponentTreeUI(myframe);
A dialog that allows the user to
change the currently installed look-and-feel of a running
application should probably let the user choose among all the
look-and-feels that are installed on the system.
An application can find out the classnames and human-readable
names of all the installed look-and-feels on a given system by
calling the static
getInstalledLookAndFeels() method
of
UIManager. In the
implementation from Sun, this method returns either a default list
of installed look-and-feels or a list obtained from the
swing.properties file of the installation.
3.22.1 Using Themes with the Metal Look-and-Feel
You can customize the colors and fonts of the default Java
look-and-feel by subclassing the
DefaultMetalTheme class that appears in the
javax.swing.plaf.metal package. When you create
a custom subclass, you can specify the six different fonts and six
different colors used by the Metal look-and-feel. For example,
you might implement a large font theme for users who have
difficulty reading the default fonts used by Metal.
If you are feeling brave and want to second-guess the skilled
designers who put the Metal look-and-feel together, you can
subclass the abstract
MetalTheme class
directly. This class defines many methods that return colors
and fonts. All of these methods, however, are implemented in
terms of the six basic font methods and six basic color methods
of the
DefaultMetalTheme class.
If you look at the
DefaultMetalTheme API,
you'll notice that the font and color methods do not return
java.awt.Font and
java.awt.Color objects as you would expect.
Instead, they return
FontUIResource
and
ColorUIResource objects. Both of these
classes are part of the
javax.swing.plaf
package and are trivial subclasses of the more familiar
Font and
Color classes.
The only thing these subclasses do is implement the
UIResource interface. But
UIResource is a marker interface, with no
methods of its own. Thus, a
FontUIResource is
a
Font object that also happens to implement
UIResource. Similarly, a
ColorUIResource is both a
Color object and a
UIResource object.
The currently installed look-and-feel assigns default values for
many properties of Swing components. A look-and-feel
implementation needs to be able to distinguish between
default values it has specified and programmer-supplied property
values. For this reason, all
look-and-feel defaults, such as colors and fonts, must implement
the
UIResource marker interface. For our
purposes here, you can subclass
DefaultMetalTheme and use the
FontUIResource and
ColorUIResource classes exactly as you would
use normal
Font and
Color
resources.
Once you have created your own theme by subclassing
MetalTheme or
DefaultMetalTheme, you can install it with
code like this:
MetalLookAndFeel.setCurrentTheme(new MyCustomTheme());
If you are changing the current theme after having already
created Swing components, you also have to reinstall the
MetalLookAndFeel and notify all the
components of the change:
UIManager.setLookAndFeel(new MetalLookAndFeel());
SwingUtilities.updateComponentTreeUI(myRootFrame);
3.22.2 Auxiliary Look-and-Feels
If you've browsed the list of Swing packages, you've probably
noticed
javax.swing.plaf.multi. This is the
multiplexing look-and-feel. It allows one or more auxiliary
look-and-feels to be used in conjunction with a single primary
look-and-feel. The multiplexing look-and-feel is automatically used
by a Swing application if an auxiliary look-and-feel has been
requested. An application can request an auxiliary
look-and-feel by calling the static
UIManager method
addAuxiliaryLookAndFeel(), while an end user can
do this by setting the
swing.auxiliarylaf
property on a Java command line.
The primary purpose of auxiliary look-and-feels is for
accessibility. For example, a person with impaired vision might
start up a Java application using the
-Dswing.auxiliarylaf= option to specify that
the application should load a screen-reader look-and-feel.
Auxiliary look-and-feels can be used for other purposes as well,
of course. You might use an auxiliary look-and-feel to add
audio feedback to a user interface. Such a look-and-feel might
produce an audible click when the user clicks on a
JButton, for example.
Swing is not shipped with any predefined auxiliary
look-and-feels. You can implement your own, of course, although
explaining how to do so is beyond the scope of this
book.
3.23 Accessibility
The term
accessibility refers to the
architectural features of Swing that allow Swing applications to
interact with assistive technologies, such as a
visual macro recorder that allows users to
automate repetitive point-and-click tasks or a screen reader.
To enable accessibility, every Swing component implements the
Accessible interface, which, like all
accessibility-related classes, is part of the
javax.accessibility package. This interface
defines a single
getAccessibleContext() method
that returns an
AccessibleContext object for
the component. The methods of
AccessibleContext export salient information
about the component, such as a list of its accessible children
and its name, purpose, and description. An
assistive technology can use the tree of
AccessibleContext objects to gather information
about a GUI and assist the user in interacting with that
GUI.
A number of the
AccessibleContext methods
return objects that implement specialized interfaces to return
specific types of accessibility information. For example, if an
accessible component
represents a numeric value of some sort
(say a
JSlider), the
getAccessibleValue() method of its
AccessibleContext object returns an
AccessibleValue object that provides more
information about that value and allows the assistive technology
to query and set the value.
The interfaces and classes of the
javax.accessibility package provide methods that
allow an assistive technology to "read" a GUI. Many of the methods
defined by these interfaces duplicate functionality already
provided by Swing components. The point, however, is that
java.accessibility defines a standard API for
interaction between any assistive technology and any accessible
application. In other words, the accessibility API is not
Swing specific. You can write JavaBeans and other custom components so
that they support accessibility. If you do, these components
automatically work with assistive technologies.
The details of the
javax.accessibility package
are of interest to programmers who are creating assistive
technologies and developing accessible
components or JavaBeans. Unfortunately, the details of these tasks are
beyond the scope of this book.
Most of us are not developing assistive technologies and only rarely do
we have to create accessible components. What we all want to do,
however, is create accessible applications. Since all Swing
components support accessibility, it is quite simple to create an
accessible application with Swing. The key to supporting
accessibility is providing the necessary information that
allows an assistive technology to interpret your GUI for a user.
The most commonly used example of an assistive technology is a
screen reader for the vision impaired. A screen reader needs to
be able to verbally describe a GUI to a user who cannot see it.
In order to do this, it needs to have names and descriptions for
all the critical components in your GUI.
The easiest way to assign a description to a component is to give
it a tooltip. This way, your accessibility information also serves
as context-sensitive help for novice users:
continue.setToolTipText("Click here to continue");
If, for some reason, you want to assign an accessible description
to a component without giving it a tooltip, you can use code like this:
continue.getAccessibleContext().setAccessibleDescription("Continue button");
It is also helpful to assistive technologies if you provide
names for your various components. A name should be a short
human-readable string that uniquely identifies the component, at
least within the current window or dialog box. Buttons, labels,
menu items, and other components that display labels simply use
those labels as their accessible names. Other components need to
have names assigned. Here is one way to do that:
JTextField zipcode = new JTextField();
zipcode.getAccessibleContext().setAccessibleName("zipcode");
In a GUI, important components that do not display their own labels
are often associated with
JLabel
components that serve to identify them. When this is the case,
you can use the
setLabelFor() method of
JLabel to set the accessible name of the other
component. The code might look like this:
JLabel zipcodeLabel = new JLabel("Zipcode");
JTextField zipcode = new JTextField();
zipcodeLabel.setLabelFor(zipcode);
By taking the simple step of assigning
names and descriptions to your GUI components, you ensure that
your application can be interpreted by assistive technologies and
successfully used by all users.
3.24 Custom Components
We'll conclude this survey of Swing features with a quick look at
what it takes to write a custom Swing component. Creating a
custom component is a matter of subclassing an existing
component and adding the new functionality you desire. Sometimes
this is a simple job of adding a minor new feature to an
existing component. At other times, you may want to create an entirely
new component from scratch. In this case, you'll probably be
subclassing
JComponent, which is a bit more
complicated. The following sections briefly
explain the various things you'll need to consider when creating
such a custom component. The best way to learn to write your own
Swing-style components is to study the source code of Swing
components, and since Sun makes this source code freely available,
I encourage you to examine it.
3.24.1 Properties
You need to decide what properties you want your component to
export and
define accessor methods that allow them to be set and queried.
If your component represents or displays some kind of
nontrivial data structure, consider representing the data in a
separate model object. Define an interface for the model and a
default implementation of the interface.
If you think that other objects may be interested in property
changes on your component, have the
set
methods for those properties generate
the events
PropertyChangeEvent or
ChangeEvent and include appropriate
event listener registration methods in your component. This
kind of notification is often important if you follow the Swing
architecture and divide the functionality of your component
among a component object, a model object, and a UI delegate
object.
When a property is set on your component, the component may need
to be redrawn or resized as a result. You must keep this in
mind when you write the property accessor methods for your
component. For example, if you define a
setColor() method, this method should
call
repaint() to request that the component
be repainted. (Painting the component is a separate topic that
is discussed later.) If you define a
setFont() method and a change in font size
causes the component to require more (or less) space on the
screen, you should call
revalidate() to
request a relayout of the GUI. Note that the
repaint() and
revalidate()
methods add a repaint or relayout request to a queue and return
right away. Therefore, you may call these methods freely without
fear of inefficiency.
3.24.2 Events
You need to decide what kind of events your component generates.
You can reuse existing event and listener classes, if they suit
your purposes, or you can define your own. Add event listener
registration and
deregistration methods in your component. You need to keep
track of the registered listeners, and you may find the
javax.swing.event.EventListenerList helpful
for this task. For each event listener registration method, it
is common practice to define a protected method to generate and
fire an appropriate event to all registered listeners. For
example, if your component has a public
addActionListener() method, you may
find it useful to define a protected
fireActionEvent() method as well. This
method calls the
actionPerformed() method
of every registered
ActionListener object.
3.24.3 Constructors
It is customary to provide a no-argument constructor for a
component. This is helpful if you want your component to work
with GUI builder tools, for example. In addition,
think about how you expect programmers to use your
component. If there are a few properties that are likely to
be set in most cases, you should define a constructor that
takes values for these properties as arguments, to make the
component easier to use.
3.24.4 Drawing the Component
Almost every component has some visual appearance. When you
define a custom component, you have to write the code that draws
the component on the screen. There are several ways you can do
this. If you are creating an AWT component, override the
paint() method and use the
Graphics object that is passed to it to do
whatever drawing you need to do.
For Swing components, the
paint() method is
also responsible for drawing the border and the children of your
component, so you should not override it directly. Instead,
override the
paintComponent() method. This
method is passed a
Graphics object, just as
the
paint() method is, and you use this
Graphics object to do any drawing you want.
As we'll see in Chapter 4,
Graphics with AWT and Java 2D, you can cast this
Graphics object to a
Graphics2D object if you want to use Java 2D
features when drawing your component. Keep in mind, however,
that a Swing component can be assigned an arbitrary border. Your
paintComponent() method should check the size
of the border and take this value into account when drawing.
When you define a custom component, you typically have only one
look-and-feel in mind, so you can hardcode this look-and-feel
as part of the component class itself. If you want
your component to support the Swing pluggable look-and-feel
architecture, however, you need to separate the drawing and
event-handling tasks out into a separate
javax.swing.plaf.ComponentUI object.
If you do this, you should not override your component's
paintComponent() method. Instead, put
the painting functionality in the
paint()
method of the
ComponentUI implementation.
In order to make this work, you have to override the
getUIClassID(),
getUI(),
setUI(), and
updateUI()
methods of
JComponent.
3.24.5 Handling Events
Most components have some kind of interactive behavior and
respond to user-input events such as mouse clicks and drags and
key presses. When you are creating a custom component, you must
write the code that handles these events. The Swing event-handling model was discussed in Chapter 2.
Recall that the high-level way to handle input events is to
register appropriate event listeners, such as
MouseListener,
MouseMotionListener,
KeyListener, and
FocusListener on your component. If you are
using a separate UI delegate object, this object should
implement the appropriate listener interfaces, and it should
register itself with the appropriate event registration methods
on the component when its
installUI() method
is called.
If you are not using a UI delegate, your component class
can handle events at the lower level discussed in Chapter 2. To do this, you override methods such
as
processMouseEvent(),
processMouseMotionEvent(),
processKeyEvent(), and
processFocusEvent(). In this case, be sure to
register your interest in receiving events of the appropriate type by
calling
enableEvents() in your component's
initialization code.
3.24.6 Component Size
Most components have a natural or preferred size that often
depends on the settings of various component properties. Many
components also have a minimum size below which they cannot
adequately display themselves. And some components have a
maximum size they wish to enforce. You must write the methods
that compute and return these sizes.
If you are using a UI delegate object, you should implement the
getMinimumSize(),
getPreferredSize(), and
getMaximumSize() methods in the delegate.
The default
JComponent methods call the
delegate methods to determine these sizes if the programmer
using the component has not
overridden the minimum, preferred, or maximum sizes with her
own specifications.
If you are not using a UI delegate object, you should
override these three methods in the component itself. Ideally,
your methods should respect any sizes passed to
setMinimumSize(),
setPreferredSize() and
setMaximumSize(). Unfortunately, the values
set by these methods are stored in
private
fields of
JComponent, so you typically
have to override both the
get and the
set methods.
3.24.7 Accessibility
It is a good idea to make your component accessible. In order
to do this, your component must implement the
javax.accessibility.Accessible interface and
its
getAccessibleContext() method.
This method must return an
AccessibleContext
object that is customized for your component. You typically
implement
AccessibleContext as an inner class
of the component by extending
JComponent.AccessibleJComponent or some
subclass of that class. Depending
on your component, you may need to implement various other
accessibility interfaces on this inner class as well. Studying
the accessibility code in existing Swing components can be very
helpful in learning how to write your own accessible components.
You might start, for example, with the source code for
AbstractButton.AccessibleAbstractButton.
3.24.8 Miscellaneous Methods
JComponent defines a number of other methods
that you can optionally override to change aspects of a
component's behavior. If you take a look at the list of
properties defined by the
JComponent API,
you'll notice that a number of these are read-only properties
(i.e., they do not define
set methods).
The only way to set the value returned by one of
these methods is to subclass the method.
In general, when you see a read-only property, you should
consider it a candidate for subclassing. Here are a few
methods of particular interest:
-
isOpaque()
-
If the component always fills its entire background, this
method should return
true. If a
component can guarantee that it completely paints itself,
Swing can perform some drawing optimizations.
JComponent actually does define a
setOpaque() method for this property,
but your custom component may choose to ignore
setOpaque() and override
isOpaque().
-
isOptimizedDrawingEnabled()
-
If your component has children and allows those children
to overlap, it should override this method to return
false. Otherwise, leave it as is.
-
isFocusTraversable()
-
If your component wants to be included in focus traversal,
it should override this method to return
true. If your component does not want
to be included in the keyboard navigation system, this method
should return
false.
-
isFocusCycleRoot()
-
If your component has children and wants to cycle focus
among them, override this method to return
true.
-
isManagingFocus()
-
If your component needs to receive the
Tab and
Shift-Tab key events that are
normally handled by the focus manager, override this
method to return
true. If you do,
the focus manager uses
Ctrl-Tab
instead.
Previous Page
|