New York
University
Courant
Institute of Mathematical Sciences
Session 3:
Detailed Review of the Swing Packages
(http://developer.java.sun.com/developer/onlineTraining/GUI/Swing1/,
http://java.sun.com/j2se/1.3/docs/guide/swing/,
http://developer.java.sun.com/developer/Books/swing2/)
Course Title: Extreme Java Course
Number: g22.3033-007
Instructor: Jean-Claude Franchitti Session: 3
I. What is
in the Swing Packages?
Swing, like any well-behaved collection of Java
software, groups its classes and interfaces into packages. Swing's packages, and their contents, are:
·
The Accessibility package: This package defines a
contract between Java UI objects (such as screen readers, Braille terminals,
and so on) and screen access products used by people with disabilities. Swing
components fully support the accessibility interfaces defined in the
accessibility package, making it easy to write programs with Swing that people
with disabilities can use. The design of the accessibility package also makes
it easy to add accessibility support to custom components. These custom
components can extend existing Swing components, can extend existing AWT components,
or can be lightweight components developed from scratch.
·
The Swing component package (javax.swingapi): The Swing component
package is the largest of Swing's five packages. As Swing's initial beta
release drew near, the Swing component package contained 99 classes and 23
interfaces. With a couple of exceptions, the Swing component package is also
the package that implements all the component classes used in Swing. (The
exceptions are JTableHeaderapi,
implemented in the javax.swing.tableapi
package, and JTextComponentapi,
implemented in javax.swing.textapi.
) Swing's UI classes (those that
have names beginning with "J") are classes that are actually used to
implement components; all other Swing classes are non-UI classes that provide
services and functionalities to applications and components. For more details,
see the section headed "Varieties of Swing classes."
·
The basic package
(javax.swing.plaf.basic): The basic package is the second largest package in the Swing set. It
contains around 57 classes (but no interfaces). This package defines the
default look-and-feel characteristics of Swing components. By subclassing the
classes in the basic package, you can create components with your own
customized pluggable L&F designs.)
·
The beaninfo package (javax.swing.beaninfo): Defines the SwingBeanInfo class, the
superclass for all Swing BeanInfo classes. This package provides default
implementations of the getIcon() and getDefaultPropertyIndex() methods, as well as utility
methods like createPropertyDescriptor(), which makes it possible
to write BeanInfo implementations. This class is meant to be used along with GenSwingBeanInfo(), a BeanInfo-class code
generator.
·
The border package (javax.swing.borderapi): This package contains one
interface and nine classes that you can subclass when you want to draw a
specialized border around a component. (You don't have to touch this package
when you create components with the default borders prescribed by whatever look
and feel you are using. Swing draws default borders automatically around
standard components.)
·
The event package (javax.swing.eventapi): The event package defines
Swing-specific event classes. Its role is similar to that of the java.awt.event
package.
Under most ordinary
conditions, you'll probably never have to subclass the components provided in
the event package. But if you ever need to create a component that needs to
monitor some other particular component -- perhaps in a special way -- you can
tap one of the classes provided in this package. Swing's JListapi
class, for example, uses this package to determine when the user selects an
item in a list.
·
The multi package
(javax.swing.plaf.multi): The multi package contains Swing's Multiplexing UI classes
(delegates), which permit components to have UIs provided by multiple
user-interface (UI) factories.
·
The pluggable L&F
package (javax.swing.plaf): The pluggable L&F package contains the classes that Swing uses to
provide its components with the pluggable look-and-feel capabilities. Most
developers will never need this package. However, if you're building a new look
and feel and you can't take the conventional approach of just subclassing the
classes in the swing.plaf.basic
package, you can start with the abstract classes in swing.plaf.
·
L&F-specific
packages that became available with the release of JFC 1.1 (Swing 1.0) include:
· javax.swing.plaf.metal.
· javax.swing.plaf.motif.
·
javax.swing.plaf.windows.
In addition, a MacOS for Macintosh computers is available separately. For
details, see the Java Software Web
site.
·
The table package (javax.swing.tableapi): The table package contains
several low-level classes that Swing uses to help you build, view, and
manipulate tables.
·
The text package (javax.swing.textapi): This package contains
classes and interfaces that deal with components which contain text.
·
The HTML package (javax.swing.text.htmlapi): This package contains an HTMLEditorKitapi
class that implements a simple text editor for HTML text files.
·
The RTF package (javax.swing.text.rtfapi): This package implements a
simple text editor for RTF (rich text format) files.
·
The tree package (javax.swing.treeapi): The classes in this
package are used to create, view, and manage tree components.
·
The undo package (javax.swing.undoapi): Developers of packages
with undo capabilitie for example, text editors -- may need to know about
classes and methods defined in this package.
Varieties of
Swing classes
As noted previously, Swing's UI classes create
components and controls that you can see on the screen, while Swing's non-UI
classes provide vital services and functionalities to Swing's control classes
and the applications that use them.
Some non-UI classes -- such as ButtonGroupapi
and ImageIconapi
-- are defined in the Swing component package, along with Swing's component
classes. But most of Swing's non-component classes are implemented in the other
packages listed in the section titled "What's in
the Swing packages
" presented earlier this document.
The JComponent
class
JComponentapi, an
abstract class, is the root class of almost of of Swing's user-interface
("J") components. JComponent's root class is the Object class, which
is defined in the java.lang.Object file.
JComponent also inherits from the AWT Container and Component classes.
JComponent implements the Accessibilityapi
and Serializable interfaces.
Swing's UI components inherit the following features
and capabilities from the JComponent class:
·
Pluggable L&F : The JComponent class
provides all JComponent-derived component classes with their "pluggable
look-and-feel" technology, which allows the developer (or, optionally, the
user) to specify or change the appearance and behavior of the Swing components
used in an application, even while the program is being executed.
·
Extensibility : JComponent and its
descendants have built-in technology that lets the developer combine and extend
existing components to create new, customized components.
·
Smart trapping of keyboard
events :
JComponent provides its descendants with comprehensive keystroke-handling
capabilities. Instead of trapping every keystroke and expecting you to discard
the ones you aren't interested in, JComponent-derived classes can use methods
implemented by the KeyStrokeapi
class to trap just the keystrokes that you do care about, and can automatically
initiate predetermined actions on specified components.
·
Customization of component
borders :
Any descendant of the JComponent class can display a customized border by
calling the setBorder()
method.
JComponent-derived classes can call setBorder() to create both decorative
borders and non-decorative borders (that is, borders that provide just margins
and padding).
Look-and-feel implementations generally initialize a
component's border property when the component is constructed. However, you can
always override the initial value for the border property with your own custom
border. The BorderFactory class provides a set of predefined borders that you
can use.
·
Easy resizing of components : JComponent provides
methods that make it easy for descendants to set the preferred, minimum, and
maximum size for a component.
·
Tool Tips -- JComponent supports the
use of tool tips -- short
descriptions of components, or tips about how to use components, that pop up
when the cursor lingers over a component.
·
Autoscrolling -- JComponent provides
automatic scrolling in a list, table, or tree when the user is dragging the
mouse.
·
Support for debugging : JComponent also provides
its descendants with access to Swing's debugGraphicsapi
mechanism, which displays the painting of components in slow motion so you can
check to see whether components are being drawn properly.
·
Support for accessibility
technology:
This capability is provided by the Accessibilityapi
interface.
·
Support for localization. JComponent provides
technology for performing text I/O in languages other than English.
The JApplet class
JApplet is a new class used to create applets in
Swing programs. Although JAppletapi
is based on the old AWT Applet class, it is used a little differently. It
provides input and painting mechanisms for its children -- something that the
AWT Applet class did not do. It also provides other kinds of special support
for applets with special requirements, such as applets that use the
JLayeredPane class and the JMenuBar class.
One important characteristic of the JApplet class is
that a JApplet object can have just one child: an object of a class named JRootPaneapi.
When you want to display a component inside an
applet, you can't simply call the add() method to add the component
to your applet; instead, must call a JRootPane method called getContentPane().
Swing
Component Classes
Summary of Swing's component classes |
|
|
||
Component |
Description |
Comments and |
|
|
Implements
a Java applet. |
The
root class for applets. |
|||
Implements
a button component. |
Swing's
basic button class. |
|||
Implements
a check-box component. |
A
class that creates and implements check boxes. |
|||
JColor- |
Displays
and manages a color-chooser dialog. |
In
the swing.preview package. |
||
Implements
a combo-box component. |
Swing's
version of the AWT Choice class. See JComboBoxapi. |
|||
The
mother of all Swing components. |
Root
class for most of Swing's UI classes. See JComponentapi. |
|||
Displays
an iconified version of a JInternalFrameapi. |
See
JDesktopIconapi. |
|||
Provides
a pluggable DesktopManagerapiobject
for |
See
JDesktopPaneapi. |
|||
Adds
enhancements to java.awt.Dialog. |
Contains
a root pane as its only child; must be managed using content panes. See JDialogapi. |
|||
JfileChooser |
Implements
a file-chooser dialog box. |
In
the swing.preview package. |
||
Adds
enhancements to java.awt.Frame. |
To
be covered in an upcoming issue; meanwhile, |
|||
Implements
a frame object that can be placed inside a JDesktopPaneapi
object to emulate a native frame window. |
To
be covered in an upcoming issue; meanwhile, see JInternalFrameapi
and JLayeredPaneapi.
|
|||
Creates
a display area for displaying read-only text, an image, or both. |
See
JLabelapi. |
|||
Can
display multiple layered panes (JInternalFrameapi
objects) inside a frame. |
To
be covered in an upcoming issue; meanwhile, see JInternalFrameapi
and JLayeredPaneapi.
|
|||
Allows
the user to select one or more objects from a list. A separate model, ListModelapi,
represents the contents of the list. |
See
JListapi. |
|||
Implements
a menu component. |
Descends
from JComponentapi
via JMenuItemapi. |
|||
Implements
a menu bar component. |
Subclasses |
|||
Implements
a menu item component. |
See
JMenuItemapi. |
|||
Displays
a dialog box that prompts the user for a choice and then passes that choice
on to the executing program. |
See
JOptionPaneapi. |
|||
Provides
a generic container for organizing other components. |
||||
Displays
a field in which the user can type a password. The text of the password does
not appear in the field as it is being typed. |
See
the JPassword- |
|||
Implements
a popup menu. |
Subclasses |
|||
Implements
a progress-bar component. |
See
JProgressBarapi. |
|||
Implements
a radio-button control. |
A
new class in Swing; subclasses JToggleButtonapi. |
|||
Implements
a radio-button menu item. |
Subclasses |
|||
Instantiates
in a single step an object made up of a glass pane, a layered pane, an
optional menu bar, and a content pane. |
See
JRootPaneapi. |
|||
Implements
a scroll-bar object. |
See
JScrollBarapi. |
|||
Implements
a scroll-pane object. |
See
JScrollPaneapi. |
|||
Implements
a menu separator object. |
See
JSeparatorapi. |
|||
Implements
a slider-bar object. |
See
JSliderapi. |
|||
Implements
a split-pane component. |
See
JSplitPaneapi. |
|||
Implements
a tabbed-pane ("property-page") component. |
See
JTabbedPaneapi. |
|||
Implements
a table component. |
See JTableapi. |
|||
Implements
a multiline area that can display editable or read-only text. |
A
subclass of JTextComponentapi,
which subclasses JComponentapi. |
|||
Implements
a text component that can be marked up with attributes to be represented
graphically. |
See
JTextPaneapi. |
|||
Implements
a two-stage button component. |
Subclasses
AbstractButtonapi. |
|||
Implements
a dockable, floatable tool bar. |
See
JToolBarapi. |
|||
Implements
a tool-tip component (a component that can display a short string, such as
the name of components or a user tip) |
See
JToolTipapi. |
|||
Displays
a set of hierarchical data in a graphical outline format. |
To
be covered in an upcoming issue; meanwhile, see JTree.
|
|||
Provides
a clipped view of an arbitrarily large component. Used by JScrollPaneapi. |
See
the JViewportapi. |
|||
Adds
enhancements to java.awt.Window. |
See
JWindowapi
and JRootPaneapi. |
II. Swing Design Goals
The overall goal for the Swing project was:
To build a set
of extensible GUI components to enable developers to more rapidly develop
powerful Java front ends for commercial applications.
To this end, the Swing team established a set of
design goals early in the project that drove the resulting architecture. These guidelines mandated that Swing would:
1.
Be implemented entirely in
Java to
promote cross-platform consistency and easier maintenance.
2.
Provide a single API capable
of supporting multiple look-and-feels so that developers and end-users would not be
locked into a single look-and-feel.
3.
Enable the power of
model-driven programming without requiring it in the highest-level API.
4.
Adhere to JavaBeansTM
design principles to ensure that components behave well in IDEs and builder tools.
5.
Provide compatibility with AWT* APIs where there is overlapping, to leverage the AWT
knowledge base and ease porting.
Swing's Roots in MVC
Swing architecture is rooted in the model-view-controller (MVC) design that dates back to SmallTalk. MVC architecture calls for a visual application to be
broken up into three separate parts:
·
A
model that represents the data for
the application.
·
The
view that is the visual
representation of that data.
·
A
controller that takes user input on
the view and translates that to changes in the model.
Early on, MVC was a logical choice for
Swing because it provided a basis for meeting the first three of our design
goals within the bounds of the latter two.
The first Swing prototype followed a
traditional MVC separation in which each component had a separate model object
and delegated its look-and-feel implementation to separate view and controller
objects.
The
delegate
It was quickly discovered that this
split didn't work well in practical terms because the view and controller parts
of a component required a tight coupling (for example, it was very difficult to
write a generic controller that didn't know specifics about the view). So the
original designers collapsed these two entities into a single UI
(user-interface) object.
Swing architecture is loosely based -- but not strictly based -- on the traditional MVC
design. In the world of Swing, this new quasi-MVC design is sometimes referred
to a separable model architecture.
Swing's separable model design treats the model part
of a component as a separate element, just as the MVC design does. But Swing
collapses the view and controller parts of each component into a single UI
(user-interface) object.
To MVC or
not to MVC?
One noteworthy point is that as an
application developer, you should think of a component's view/controller
responsibilities as being handled by the generic component class (such as. JButton, JTree, and so on).
The component class then delegates the look-and-feel-specific aspects of those
responsibilities to the UI object that is provided by the currently installed
look-and-feel.
For example, the code that implements
double-buffered painting is in Swing's JComponent class (the
"mother" of most Swing component classes), while the code that
renders a JButton's label is in the button's UI delegate class.
So Swing does have a strong MVC lineage. But it's
also important to reiterate that the MVC architecture serves two distinct
purposes:
·
First, separating the model definition from a component facilitates
model-driven programming in Swing.
·
Second,
the ability to delegate some of a component's view/controller responsibilities
to separate look-and-feel objects provides the basis for Swing's pluggable
look-and-feel architecture.
Although these two concepts are linked by the MVC
design, they may be treated somewhat orthogonally from the developer's
perspective. The remainder of this document will cover each of these mechanisms
in greater detail.
Separable
Model Architecture
It is generally considered good practice to center
the architecture of an application around its data rather than around its user
interface. To support this paradigm, Swing defines a separate model interface
for each component that has a logical data
or value abstraction. This separation
provides programs with the option of plugging in their own model
implementations for Swing components.
The following table shows the component-to-model
mapping for Swing.
Component |
Model Interface |
Model Type |
JButton |
ButtonModel |
GUI |
JToggleButton |
ButtonModel |
GUI/data |
JCheckBox |
ButtonModel |
GUI/data |
JRadioButton |
ButtonModel |
GUI/data |
JMenu |
ButtonModel |
GUI |
JMenuItem |
ButtonModel |
GUI |
JCheckBoxMenuItem |
ButtonModel |
GUI/data |
JRadioButtonMenuItem |
ButtonModel |
GUI/data |
JComboBox |
ComboBoxModel |
data |
JProgressBar |
BoundedRangeModel |
GUI/data |
JScrollBar |
BoundedRangeModel |
GUI/data |
JSlider |
BoundedRangeModel |
GUI/data |
JTabbedPane |
SingleSelectionModel |
GUI |
JList |
ListModel |
data |
JList |
ListSelectionModel |
GUI |
JTable |
TableModel |
data |
JTable |
TableColumnModel |
GUI |
JTree |
TreeModel |
data |
JTree |
TreeSelectionModel |
GUI |
JEditorPane |
Document |
data |
JTextPane |
Document |
data |
JTextArea |
Document |
data |
JTextField |
Document |
data |
JPasswordField |
Document |
data |
GUI-state vs. application-data models
The
models provided by Swing fall into two general categories:
GUI-state models and application-data models.
GUI-state
models
GUI state
models are interfaces
that define the visual status of a GUI control, such as whether a button is
pressed or armed, or which items are selected in a list. GUI-state models
typically are relevant only in the context of a graphical user interface (GUI).
While it is often useful to develop programs using
GUI-state model separation -- particularly if multiple GUI controls are linked
to a common state (such as in a shared whiteboard program), or if manipulating
one control automatically changes the value of another -- the use of GUI-state
models is not required by Swing. It is possible to manipulate the state of a
GUI control through top-level methods on the component, without any direct
interaction with the model at all. In the preceding table, GUI-state models in
Swing are highlighted in blue.
Application-data
models
An application-data
model is an interface that represents some quantifiable data that has
meaning primarily in the context of the application, such as the value of a
cell in a table or the items displayed in a list. These data models provide a
very powerful programming paradigm for Swing programs that need a clean
separation between their application data/logic and their GUI. For truly
data-centric Swing components, such as JTree and JTable, interaction with the
data model is strongly recommended. Application-data models are highlighted in red in the table presented at the
beginning of this section.
Of course with some components, the model
categorization falls somewhere in between GUI state
models and application-data models, depending on the context in which the model
is used. This is the case with the BoundedRangeModel on JSlider or JProgressBar. These models are
highlighted in purple in the preceding table.
Swing's separable model API makes no specific
distinctions between GUI state models and
application-data models; however, this difference
has been clarified here to give developers a better understanding of when and
why they might wish to program with the separable models.
Shared model definitions
Referring again to the table at
the beginning of this section, notice that model definitions are shared across
components in cases where the data abstraction
for each component is similar enough to support a single interface without
over-genericizing that interface.
Common models enable automatic connectability
between component types. For example, because both JSlider and JScrollbar use the BoundedRangeModel interface, a single BoundedRangeModel instance could be plugged
into both a JScrollbar and a JSlider and their visual state
would always remain in sync.
The separable-model API
Swing components that define models support a JavaBeans bound property for the model.
For example, JSlider uses the BoundedRangeModel interface for its model
definition.
Consequently,
it includes the following methods:
public
BoundedRangeModel getModel()
public void
setModel(BoundedRangeModelmodel)
All Swing components have one thing in common: If
you don't set your own model, a default is created and installed internally in
the component. The naming convention for these default model classes is to
prepend the interface name with "Default." For JSlider, a DefaultBoundedRangeModel object is instantiated in
its constructor:
public
JSlider(int orientation, int min,
int max, int value)
{
checkOrientation(orientation);
this.orientation = orientation;
this.model
=
new
DefaultBoundedRangeModel(value, 0, min, max);
this.model.addChangeListener(changeListener);
updateUI();
}
If a program subsequently calls setModel(), this default model is
replaced, as in the following example:
JSlider
slider = new JSlider();
BoundedRangeModel
myModel =
new DefaultBoundedRangeModel() {
public void setValue(int n) {
System.out.println("SetValue:
"+ n);
super.setValue(n);
}
});
slider.setModel(myModel);
For more complex models (such as those for JTable and JList), an abstract model
implementation is also provided to enable developers to create their own models
without starting from scratch. These classes are prepended with
"Abstract".
For example, JList's model interface is ListModel, which provides both DefaultListModel and AbstractListModel classes to help the
developer in building a list model.
Model change notification
Models must be able to notify any interested parties
(such as views) when their data or value changes. Swing models use the JavaBeans Event model for the
implementation of this notification. There are two approaches for this
notification used in Swing:
·
Send
a lightweight notification that the
state has "changed" and require the listener to respond by sending a
query back to the model to find out what
has changed. The advantage of this approach is that a single event instance can
be used for all notifications from a particular model -- which is highly
desirable when the notifications tend to be high in frequency (such as when a JScrollBar is dragged).
·
Send
a stateful notification that
describes more precisely how the
model has changed. This alternative requires a new event instance for each
notification. It is desirable when a generic notification doesn't provide the
listener with enough information to determine efficiently what has changed by
querying the model (such as when a column of cells change value in a JTable).
Lightweight
notification
The following models in Swing use the lightweight notification, which is based
on the ChangeListener/ChangeEvent API:
Model |
Listener |
Event |
The
ChangeListener
interface has a single generic method:
public void stateChanged(ChangeEvent e)
The only state in a ChangeEvent
is the event "source." Because the source is always the same across
notifications, a single instance can be used for all notifications from a
particular model. Models that use this mechanism support the following methods
to add and remove ChangeListeners:
public void
addChangeListener(ChangeListener l)
public void
removeChangeListener(ChangeListener l)
Therefore, to be notified when the value of a JSlider has changed, the code might
look like this:
JSlider
slider = new JSlider();
BoundedRangeModel
model = slider.getModel();
model.addChangeListener(new
ChangeListener() {
public void stateChanged(ChangeEvent e) {
// need to query the model
// to get updated value...
BoundedRangeModel m =
(BoundedRangeModel)e.getSource();
System.out.println("model changed:
" +
m.getValue());
}
});
To provide convenience for programs that don't wish
to deal with separate model objects, some Swing component classes also provide
the ability to register ChangeListeners directly on the component (so the
component can listen for changes on the model internally and then propagates
those events to any listeners registered directly on the component). The only
difference between these notifications is that for the model case, the event
source is the model instance, while for the component case, the source is the
component.
So the preceding example could be simplified to:
JSlider
slider = new JSlider();
slider.addChangeListener(new
ChangeListener() {
public void stateChanged(ChangeEvent e) {
// the source will be
// the slider this time..
JSlider s = (JSlider)e.getSource();
System.out.println("value changed:
" +
s.getValue());
}
});
Stateful notification
Models that support stateful notification provide event Listener interfaces and event
objects specific to their purpose. The following table shows the breakdown for
those models:
Model |
Listener |
Event |
The usage of these APIs is similar to the lightweight notification, except that the listener can query the event object directly to find out what has changed. For example, the following code dynamically tracks the selected item in a JList:
String
items[] = {"One", "Two", "Three");
JList list
= new JList(items);
ListSelectionModel
sModel = list.getSelectionModel();
sModel.addListSelectionListener
(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent
e) {
// get change information directly
// from the event instance...
if (!e.getValueIsAdjusting()) {
System.out.println("selection
changed: " +
e.getFirstIndex());
}
}
});
Automatic
View Updates
A model does not have any intrinsic knowledge of the view that represents it. (This requirement is critical to enable multiple views on the same model). Instead, a model has only a list of listeners interested in knowing when its state has changed. A Swing component takes responsibility for hooking up the appropriate model listener so that it can appropriately repaint itself as the model changes (if you find that a component is not updating automatically when the model changes, it is a bug!). This is true whether a default internal model is used or whether a program installs its own model implementation.
Ignoring models completely
As mentioned previously, most components provide the
model-defined API directly in the component class so that the component can be
manipulated without interacting with the model at all. This is considered
perfectly acceptable programming practice (especially for the GUI-state
models). For example, following is JSlider's implementation of getValue(), which internally delegates
the method call to its model:
public int
getValue() {
return getModel().getValue();
}
And
so programs can simply do the following:
JSlider
slider = new JSlider();
int value =
slider.getValue();
//what's a
"model," anyway?
Separable model summary
So while it's useful to understand how Swing's model
design works, it isn't necessary to use the model API for all aspects of Swing
programming. You should carefully consider your application's individual needs
and determine where the model API will enhance your code without introducing
unnecessary complexity.
In particular, we recommend the usage of the
Application-Data category of models for Swing (models for JTable, JTree, and the like) because they
can greatly enhance the scalability and modularity of your application over the
long run.
Swing's pluggable look-and-feel architecture allows us to provide a single component API without dictating a particular look-and-feel.
The Swing toolkit provides a default set of
look-and-feels; however, the API is "open" -- a design that
additionally allows developers to create new look-and-feel implementations by
either extending an existing look-and-feel or creating one from scratch.
Although the pluggable look-and-feel API is
extensible, it was intentionally designed at a level below the basic component
API in such a way that a developer does not need to understand its intricate
details to build Swing GUIs. (But if you want
to know, read on . . .)
While we don't expect (or advise) the majority of
developers to create new look-and-feel implementations, we realize PL&F is
a very powerful feature for a subset of applications that want to create a
unique identity. As it turns out, PL&F is also ideally suited for use in
building GUIs that are accessible to users with disabilities, such as visually
impaired users or users who cannot operate a mouse.
In a nutshell, pluggable look-and-feel design simply
means that the portion of a component's implementation that deals with the
presentation (the look) and event-handling (the feel) is delegated to a
separate UI object supplied by the currently installed look-and-feel, which can
be changed at runtime.
The pluggable look-and-feel API
The
pluggable look-and-feel API includes:
· Some small hooks in the
Swing component classes.
· Some top-level API for
look-and-feel management.
· A more complex API that
actually implements look-and-feels in separate packages.
The
component hooks
Each Swing component that has look-and-feel-specific
behavior defines an abstract class in the swing.plaf package to represent its UI
delegate. The naming convention for these classes is to take the class name for
the component, remove the "J" prefix, and append "UI." For
example, JButton defines its UI delegate
with the plaf class ButtonUI.
The UI delegate is created in the component's
constructor and is accessible as a JavaBeans bound property on the component.
For example, JScrollBar provides the following
methods to access its UI delegate:
public ScrollBarUI getUI()
public void setUI(ScrollBarUI ui)
This process of creating a UI delegate and setting
it as the "UI" property for a component is essentially the
"installation" of a component's look-and-feel.
Each component also provides a method which creates
and sets a UI delegate for the "default" look-and-feel (this method
is used by the constructor when doing the installation):
public void updateUI()
A look-and-feel implementation provides concrete
subclasses for each abstract plaf UI
class. For example, the Windows look-and-feel defines WindowsButtonUI, a WindowsScrollBarUI, and so on. When a
component installs its UI delegate, it must have a way to look up the
appropriate concrete class name for the current default look-and-feel
dynamically. This operation is performed using a hash table in which the key is
defined programmatically by the getUIClassID() method in the component. The convention is to use
the plaf abstract class name for
these keys. For example, JScrollbar provides:
public
String getUIClassID() {
return "ScrollBarUI";
}
Consequently, the hash table in the Windows
look-and-feel will provide an entry that maps "ScrollBarUI" to
"com.sun.java.swing.plaf.windows.WindowsScrollBarUI"
Look-and-feel
management
Swing defines an abstract LookAndFeel
class that represents all the information central to a look-and-feel
implementation, such as its name, its description, whether it's a native
look-and-feel -- and in particular, a hash table (known as the "Defaults
Table") for storing default values for various look-and-feel attributes,
such as colors and fonts.
Each look-and-feel
implementation defines a subclass of LookAndFeel (for example, swing.plaf.motif.
MotifLookAndFeel) to provide Swing with the necessary information to manage the
look-and-feel.
The UIManager
is the API through which components and programs access look-and-feel
information (they should rarely, if ever, talk directly to a LookAndFeel instance). UIManager is responsible for keeping
track of which LookAndFeel classes are available,
which are installed, and which is currently the default.
The UIManager also manages access to the Defaults Table for the
current look-and-feel.
The
'default' look and feel
The UIManager also provides methods for getting and setting the
current default LookAndFeel:
public static LookAndFeel
getLookAndFeel()
public static void
setLookAndFeel(LookAndFeel
newLookAndFeel)
public static void
setLookAndFeel(String className)
As a default look-and-feel, Swing initializes the
cross-platform JavaTM look and feel (formerly known as
"Metal"). However, if a Swing program wants to set the default
Look-and-Feel explicitly, it can do that using the UIManager.setLookAndFeel() method. For example, the
following code sample will set the default Look-and-Feel to be CDE/Motif:
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.motif.MotifLookAndFeel");
Sometimes an application may not want to specify a
particular look-and-feel, but instead wants to configure a look-and-feel in
such a way that it dynamically matches whatever platform it happens to be
running on (for instance, the. Windows look-and-feel if it is running on
Windows NT, or CDE/Motif if it running on Solaris). Or, perhaps, an application
might want to lock down the look-and-feel to the cross-platform Java look and
feel.
The UIManager provides the following static methods to
programmatically obtain the appropriate LookAndFeel class names for each of
these cases:
public static String
getSystemLookAndFeelClassName()
public static String
getCrossPlatformLookAndFeelClassName()
So, to ensure that a program always runs in the platform's system look-and-feel, the code might look like this:
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
Dynamically
Changing the Default Look-and-Feel
When a Swing application programmatically sets the
look-and-feel (as described above), the ideal place to do so is before any Swing components are instantiated.
This is because the UIManager.setLookAndFeel() method makes a particular LookAndFeel the current default by
loading and initializing that LookAndFeel instance, but it does not automatically cause any existing components to change their
look-and-feel.
Remember that components initialize their UI
delegate at construct time,
therefore, if the current default changes after they are constructed, they will
not automatically update their UIs
accordingly. It is up to the program to implement this dynamic switching by
traversing the containment hierarchy and updating the components individually.
(NOTE: Swing provides the SwingUtilities.updateComponentTreeUI() method to assist with this
process).
The look-and-feel of a component can be updated at
any time to match the current default by invoking its updateUI() method, which uses the
following static method on UIManager to get the appropriate UI
delegate:
public static ComponentUI getUI(JComponent
c)
For example, the implementation of updateUI() for the JScrollBar looks
like the following:
public void
updateUI() {
setUI((ScrollBarUI)UIManager.getUI(this));
}
And so if a program needs to change the look-and-feel of a GUI hierarchy after it was instantiated, the code might look like the following:
// GUI
already instantiated, where myframe
// is
top-level frame
try {
UIManager.setLookAndFeel(
"com.sun.java.swing.plaf.motif.MotifLookAndFeel");
myframe.setCursor(
Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
SwingUtilities.updateComponentTreeUI(myframe);
myframe.validate();
} catch
(UnsupportedLookAndFeelException e) {
} finally {
myframe.setCursor
(Cursor.getPredefinedCursor
(Cursor.DEFAULT_CURSOR));
}
Managing
look-and-feel data
The UIManager defines a static class, named UIManager.LookAndFeelInfo, for storing the high-level
name (such as. "Metal") and particular class name (such as "com.sun.java.swing.plaf.MetalLookAndFeel")
for a LookAndFeel. It uses these classes
internally to manage the known LookAndFeel objects. This information can be accessed from the UIManager via the following static
methods:
public static LookAndFeelInfo[]
getInstalledLookAndFeels()
public static void
setInstalledLookAndFeels(LookAndFeelInfo[] infos)
throws SecurityException
public static void
installLookAndFeel(LookAndFeelInfo info)
public static void
installLookAndFeel(String name, String
className)
These methods can be used to programmatically determine which look-and-feel implementations are available, which is useful when building user interfaces which allow the end-user to dynamically select a look-and-feel.
The
look-and-feel packages
The UI delegate classes provided in swing.plaf (ButtonUI, ScrollBarUI, and so on) define the
precise API that a component can use to interact with the UI delegate instance.
(NOTE: Interfaces were originally
used here, but they were replaced with abstract classes because we felt the API
was not mature enough to withstand the concrete casting of an interface.)
These plaf
APIs are the root of all look-and-feel implementations.
Each look-and-feel implementation provides concrete
subclasses of these abstract plaf classes. All such classes defined by a
particular look-and-feel implementation are contained in a separate package
under the swing.plaf package (for example,. swing.plaf.motif,
swing.plaf.metal, and so on). A look-and-feel package contains the following:
·
The
LookAndFeel subclass:
e.g., MetalLookAndFeel
·
All
look-and-feel's UI delegate classes (for example, MetalButtonUI, MetalTreeUI, and the like).
·
Any
look-and-feel utility classes such as the following classes:
MetalGraphicsUtils
MetalIconFactory,
and so on.
·
Other
resources associated with the look-and-feel, such as image files.
In implementing the various Swing look-and-feels, we
soon discovered that there was a lot of commonality among them. We factored out
this common code into a base look-and-feel implementation (called "basic") which extends the plaf
abstract classes and from which the specific look-and-feel implementations (motif, windows, and so on.) extend. The basic look-and-feel package supports building a desktop-level
look-and-feel, such as Windows or CDE/Motif.
The basic look-and-feel package is just one example
of how to build a pluggable look-and-feel; the architecture is flexible enough
to accommodate other approaches as well.
The remainder of this document will show how a
look-and-feel package works at the generic level.
The
LookAndFeel Subclass
The LookAndFeel
class defines the following abstract methods, which all subclasses must
implement:
public String getName();
public String getID();
public String getDescription();
public boolean isNativeLookAndFeel();
public boolean isSupportedLookAndFeel();
The getName(), getID(), and getDescription() methods provide generic
information about the look-and-feel.
The isNativeLookAndFeel() method returns true if the look-and-feel is
native to the current platform. For example, MotifLookAndFeel returns true if it is currently running
on the Solaris platform, and returns false otherwise.
The isSupportedLookAndFeel() method returns whether or
not this look-and-feel is authorized to run on the current platform. For
example, WindowsLookAndFeel returns true only if it is running on a Windows 95, Windows 98, or Windows NT
machine.
A LookAndFeel class also provides methods for initialization and
uninitialization:
public void initialize()
public void uninitialize()
The initialize() method is invoked by the UIManager when the LookAndFeel is made the
"default" using the UIManager.setLookAndFeel() method.
uninitialize()is invoked by the UIManager when the LookAndFeel is about to be replaced as
the default.
The
Defaults Table
Finally, the LookAndFeel class provides a method to
return the look-and-feel's implementation of the Defaults Table:
public UIDefaults getDefaults()
The Defaults Table is represented by the UIDefaults
class, a direct extension of java.util.Hashtable, which adds methods for accessing specific
types of information about a look-and-feel. This table must include all the UIClassID-to-classname mapping information, as
well as any default values for presentation-related properties (such as color,
font, border, and icon) for each UI delegate. For example, following is a
sample of what a fragment of getDefaults() might look like for a hypothetical look-and-feel in
a package called "mine":
public
UIDefaults getDefaults() {
UIDefaults table = new UIDefaults();
Object[] uiDefaults = {
"ButtonUI", "mine.MyButtonUI",
"CheckBoxUI",
"mine.MyCheckBoxUI",
"MenuBarUI", "mine.MyMenuBarUI",
...
"Button.background",
new ColorUIResource(Color.gray),
"Button.foreground",
new ColorUIResource(Color.black),
"Button.font",
new
FontUIResource("Dialog", Font.PLAIN, 12),
"CheckBox.background",
new ColorUIResource(Color.lightGray),
"CheckBox.font",
new
FontUIResource("Dialog", Font.BOLD, 12),
...
}
table.putDefaults(uiDefaults);
return table;
}
When
the default look-and-feel is set with
UIManager.setLookAndFeel()
the UIManager calls getDefaults() on the new LookAndFeel instance and stores the
hash table it returns. Subsequent calls to the UIManager's lookup methods will be
applied to this table. For example, after making "mine" the default
Look-and-Feel:
UIManager.get("ButtonUI")
=> "mine.MyButtonUI"
The UI classes access their default information in
the same way. For example, our example ButtonUI class would initialize the JButton's "background"
property like this:
button.setBackground(
UIManager.getColor("Button.background");
The defaults are organized this way to allow
developers to override them.
Distinguishing between UI-set and app-set properties
Swing allows applications to set property values
(such as color and font) individually on components. So it's critical to make
sure that these values don't get clobbered when a look-and-feel sets up its
"default" properties for the component.
This is not an issue the first time a UI delegate is installed on a component (at construct
time) because all properties will be uninitialized and legally settable by the
look-and-feel. The problem occurs when the application sets individual
properties after component
construction and then subsequently sets a new
look-and-feel (that is, dynamic
look-and-feel switching). This means that the look-and-feel must be able to
distinguish between property values set by the application, and those set by a
look-and-feel.
This issue is handled by marking all values set by
the look-and-feel with the plaf.UIResource interface. The plaf package provides a set of
"marked" classes for representing these values:
ColorUIResource
FontUIResource
BorderUIResource
The preceding code example shows the usage of these
classes to mark the default property values for the hypothetical MyButtonUI class.
The UI delegate
The superclass of all UI Delegate classes is:
swing.plaf.ComponentUI
This class contains the primary
"machinery" for making the pluggable look-and-feel work. Its methods
deal with UI installation and uninstallation, and with delegation of a
component's geometry-handling and painting.
Many of the UI Delegate subclasses also provide
additional methods specific to their own required interaction with the
component; however, this document focuses primarily on the generic mechanism
implemented by ComponentUI.
UI
installation and deinstallation
First off, the ComponentUI class defines these
methods methods
for UI delegate installation and uninstallation:
public void installUI(JComponent c)
public void uninstallUI(JComponent c)
Looking at the implementation of JComponent.setUI() (which is always invoked
from the setUI method on JComponent
subclasses), we can clearly see how UI delegate installation/de-installation
works:
protected
void setUI(ComponentUI newUI) {
if (ui != null) {
ui.uninstallUI(this);
}
ComponentUI oldUI = ui;
ui = newUI;
if (ui != null) {
ui.installUI(this);
}
invalidate();
firePropertyChange("UI", oldUI,
newUI);
}
UI installation illustrated
|
The UI delegate's installUI() method is responsible for
the following:
·
Set
default font, color, border, and opacity properties on the component.
·
Install
an appropriate layout manager on the component.
·
Add
any appropriate child subcomponents to the component
·
Register
any required event listeners on the component.
·
Register
any look-and-feel-specific keyboard actions (mnemonics, etc.) for the
component.
·
Register
appropriate model listeners to be notified when to repaint.
·
Initialize
any appropriate instance data.
For example, the installUI() method for an extension of
ButtonUI might look like this:
protected
MyMouseListener mouseListener;
protected
MyChangeListener changeListener;
public void
installUI(JComponent c) {
AbstractButton b = (AbstractButton)c;
// Install default colors & opacity
Color bg = c.getBackground();
if (bg == null || bg instanceof UIResource)
{
c.setBackground(
UIManager.getColor("Button.background"));
}
Color fg = c.getForeground();
if (fg == null || fg instanceof UIResource)
{
c.setForeground(
UIManager.getColor("Button.foreground"));
}
c.setOpaque(false);
//
Install listeners
mouseListener = new MyMouseListener();
c.addMouseListener(mouseListener);
c.addMouseMotionListener(mouseListener);
changeListener = new MyChangeListener();
b.addChangeListener(changeListener);
}
Conventions
for initializing component properties
Swing defines a number of conventions for
initializing component properties at install-time, including the following:
1.
All
values used for setting colors, font, and border properties should be obtained
from the Defaults table (as described in the subsection on the LookAndFeel subclass).
2.
Color,
font and border properties should be set if -- and only if -- the application has not already set them.
To facilitate convention No
1, the UIManager class provides a number of
static methods to extract property values of a particular type (for instance,
the static methods:
UIManager.getColor()
UIManager.getFont()
etc.
Convention No. 2 is
implemented by always checking for either a null value or an instance of UIResource before setting the
property.
The ComponentUI's uninstall() method must carefully undo everything that was done
in the installUI() method so that the
component is left in a pristine state for the next UI delegate. The uninstall()method is responsible for:
·
Clearing
the border property if it has been set by installUI().
·
Remove
the layout manager if it had been set by installUI().
·
Remove
any subcomponents added by installUI().
·
Remove
any event/model listeners that were added by installUI().
·
Remove
any look-and-feel-specific keyboard actions that were installed by installUI().
·
Nullify
any initialized instance data (to allow GC to clean up).
For example, an uninstall() method to undo what we did
in the above example installation might look like this:
public void
uninstallUI(JComponent c) {
AbstractButton b = (AbstractButton)c;
// Uninstall listeners
c.removeMouseListener(mouseListener);
c.removeMouseMotionListener(mouseListener);
mouseListener = null;
b.removeChangeListener(changeListener);
changeListener = null;
}
Defining
geometry
In the AWT (and thus in Swing) a container's
LayoutManager will layout the child components according to its defined
algorithm; this is known as "validation" of a containment hierarchy.
Typically LayoutManagers will query the child components' preferredSize
property (and sometimes minimumSize and/or maximumSize as well, depending on
the algorithm) in order to determine precisely how to position and size those
children.
Obviously, these geometry properties are something
that a look-and-feel usually needs to define for a given component, so ComponentUI provides the following
methods for this purpose:
public Dimension
getPreferredSize(JComponent c)
public Dimension
getMinimumSize(JComponent c)
public Dimension
getMaximumSize(JComponent c)
public boolean
contains(JComponent c, int x, int y)
JComponent's parallel methods (which
are invoked by the LayoutManager during validation) then simply delegate to the
UI object's geometry methods if the geometry property was not explicitly set by
the program. Below is the implementation of JComponent.getPreferredSize() which shows this
delegation:
public
Dimension getPreferredSize() {
if (preferredSize != null) {
return preferredSize;
}
Dimension size = null;
if
(ui != null) {
size = ui.getPreferredSize(this);
}
return (size != null) ? size :
super.getPreferredSize();
}
Even though the bounding box for all components is a
Rectangle, it's possible to simulate a non-rectangular component by overriding
the implementation of the contains() method from java.awt.Component. (This method is used for
the hit-testing of mouse events). But, like the other geometry properties in
Swing, the UI delegate defines its own version of the contains() method, which is also
delegated to by
JComponent.contains():
public
boolean contains(JComponent c, int x, int y) {
return (ui
!= null) ?
ui.contains(this, x, y) :
super.contains(x, y);
}
So a UI delegate could provide non-rectangular
"feel" by defining a particular implementation of contains() (for example, if we wanted
our MyButtonUI class to implement a button
with rounded corners).
Painting
Finally,
the UI delegate must paint the component appropriately, hence ComponentUI has the following methods:
public void
paint(Graphics g, JComponent c)
public void
update(Graphics g, JComponent c)
And
once again, JComponent.paintComponent() takes care to delegate the
painting:
protected
void paintComponent(Graphics g) {
if (ui != null) {
Graphics scratchGraphics =
SwingGraphics.createSwingGraphics(g.create());
try {
ui.update(scratchGraphics,
this);
}
finally {
scratchGraphics.dispose();
}
}
}
Similarly to the way in which things are done in
AWT, the UI delegate's update() method clears the
background (if opaque) and then invokes its paint() method, which is ultimately
responsible for rendering the contents of the component.
Stateless
vs. stateful delegates
All the methods on ComponentUI take a JComponent object as a parameter. This
convention enables a stateless implementation of a UI delegate (because the
delegate can always query back to the specified component instance for state
information). Stateless UI delegate implementations allow a single UI delegate
instance to be used for all instances of that component class, which can
significantly reduce the number of objects instantiated.
This approach works well for many of the simpler GUI
components. But for more complex components, we found it not to be a "win"
because the inefficiency created by constant state recalculations was worse
than creating extra objects (especially since the number of complex GUI
components created in a given program tends to be small).
The ComponentUI class defines a static method for returning a
delegate instance:
public static ComponentUI
createUI(JComponent c)
It's the implementation of this method that
determines whether the delegate is stateless or stateful. That's because the UIManager.getUI() method invoked by the
component to create the UI delegate internally invokes this createUI method on the delegate
class to get the instance.
The Swing look-and-feel implementations use both
types of delegates. For example, Swing's BasicButtonUI class implements a
stateless delegate:
// Shared
UI object
protected
static ButtonUI buttonUI;
public
static ComponentUI createUI(JComponent c)
if(buttonUI == null) {
buttonUI = new BasicButtonUI();
}
return buttonUI;
}
While Swing's BasicTabbedPaneUI uses the stateful approach:
public
static ComponentUI createUI(JComponent c)
return new BasicTabbedPaneUI();
}
Pluggable Look-and-Feel summary
The pluggable look-and-feel feature of Swing is both
powerful and complex (which you understand if you've gotten this far!). It is
designed to be programmed by a small subset of developers who have a particular
need to develop a new look-and-feel implementation. In general, application
developers only need to understand the capabilities of this mechanism in order
to decide how they wish to support
look-and-feels (such as whether to lock-down the program to a single
look-and-feel or support look-and-feel configuration by the user). Swing's UIManager provides the API for
applications to manage the look-and-feel at this level.