JList Programming
- Part 1
Advanced JList Programming
This is Part 1 of a two-part article on JList programming.
Part 2, "Mastering JList Programming," will be published
in an upcoming issue of The Swing Connection.
By Hans Muller
|
JLIST
SAMPLE CODE
|
|
Four code samples are provided with this
article:
|
|
|
| There's also a zip file that contains
all four of these source-code files, along with object code
and resources: |
|
|
The Swing JList component shows a simple list of objects in a single
column. The user can select list entries with the mouse. JList supports
the usual selection modes -- single and interval.
The JList component implements Swing's modified model-view-controller
(MVC) architecture, which is described in "Getting
Started with Swing" Separate delegate "model" objects
define the contents of the list and the indices of the currently
selected list items.
Another characteristic of JList is that it delegates the rendering
of individual list items, or cells, to a ListCellRenderer
object. The JList API was structured this way to provide enough
flexibility for a wide variety of applications.
In this article, we'll briefly review JList basics and then take
a look at several interesting examples that demonstrate how to use
the JList API and how to optimize JList performance. In the second
part of this article: more examples!
The JList Component: A Review
In the following code snippet, we create a JList that displays
the strings in a data[]
array:
String[] data = {"one", "two", "free", "four"};
JList dataList = new JList(data);
The value of the JList model property is an object that
provides a read-only view of the data. It is constructed automatically:
for(int i = 0; i < dataList.getModel().getSize(); i++) {
System.out.println(dataList.getModel().getElementAt(i));
}
It's equally easy to create a JList that displays the contents
of a Vector. In the following example, we create a JList that displays
the superclasses of JList.class.
We store the superclasses in a java.util.Vector
and then use the JList Vector
constructor:
Vector superClasses = new Vector();
Class rootClass = javax.swing.JList.class;
for(Class cls = rootClass; cls != null; cls = cls.getSuperclass()) {
superClasses.addElement(cls);
}
JList classList = new JList(superClasses);
|
Scrolling
JList doesn't support scrolling directly. To create a scrolling
list, you make the JList the viewport view of a JScrollPane, as
in this example:
JScrollPane scrollPane = new JScrollPane(dataList);
// Or in two steps:
JScrollPane scrollPane = new JScrollPane();
scrollPane.getViewport().setView(dataList);
Selection
By default, JList supports single selection; that is, either zero
or one index can be selected. The selection state is actually managed
by a separate delegate object, an implementation of ListSelectionModel.
JList provides convenient properties for managing
the selection, however.
String[] data = {"one", "two", "free", "four"};
JList dataList = new JList(data);
dataList.setSelectedIndex(1); // select "two"
dataList.getSelectedValue(); // returns "two"
Lists with Dynamic Contents
The contents of a JList can be dynamic; that is, the list elements
can change value and the size of the list can change after the JList
is created. The JList observes changes in its model with a swing.event.ListDataListener
implementation. A correct implementation of ListModel
notifies its listeners each time a change occurs. The changes are
characterized by a swing.event.ListDataEvent, which
identifies the range of list indices that have been modified, added,
or removed.
Simple dynamic-content JList applications can use the DefaultListModel
class to store list elements. This class implements the ListModel
interface and provides the java.util.Vector API as
well. Applications that need to provide custom ListModel
implementations can subclass AbstractListModel, which
provides basic ListDataListener support. For example:
// This list model has about 2^16 elements. Enjoy scrolling.
ListModel bigData = new AbstractListModel() {
public int getSize() { return Short.MAX_VALUE; }
public Object getElementAt(int index) { return "Index " + index; }
};
JList bigDataList = new List(bigData);
|
List Geometry: fixedCellWidth, fixedCellHeight
We don't want the JList implementation to compute the width or
height of all of the list cells, so we give it a String that's as
big as we'll need for any cell. It uses this to compute values for
the fixedCellWidth and fixedCellHeight properties.
bigDataList.setPrototypeCellValue("Index 1234567890");
Cell Renderers
JList uses a java.awt.Component, provided by a delegate
called the cellRendererer, to paint the visible cells
in the list. The cell renderer component is used like a "rubber
stamp" to paint each visible row. Each time the JList needs to paint
a cell, it asks the cell renderer for the component, moves it into
place using setBounds(), and then draws it by calling
its paint method. The default cell renderer uses a JLabel
component to render the string value of each component. You can
substitute your own cell renderer, using code like this:
class MyCellRenderer extends DefaultListCellRenderer {
final static ImageIcon longIcon = new ImageIcon("long.gif");
final static ImageIcon shortIcon = new ImageIcon("short.gif");
/* This is the only method defined by ListCellRenderer. We just
* reconfigure the Jlabel each time we're called.
*/
public Component getListCellRendererComponent(
JList list,
Object value, // value to display
int index, // cell index
boolean iss, // is the cell selected
boolean chf) // the list and the cell have the focus
{
/* The DefaultListCellRenderer class will take care of
* the JLabels text property, it's foreground and background
* colors, and so on.
*/
super.getListCellRendererComponent(list, value, index, iss, chf);
/* We additionally set the JLabels icon property here.
*/
String s = value.toString();
setIcon((s.length > 10) ? longIcon : shortIcon);
return this;
}
}
String[] data = {"one", "two", "free", "four"};
JList dataList = new JList(data);
dataList.setCellRenderer(new MyCellRenderer());
|
Double-Click Handling
JList doesn't provide any special support for handling double or
triple (or n) mouse clicks; however, it's easy to handle
them using a MouseListener. Use the JList method locationToIndex()
to determine what cell was clicked. For example:
final JList list = new JList(dataModel);
MouseListener mouseListener = new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
int index = list.locationToIndex(e.getPoint());
System.out.println("Double clicked on Item " + index);
}
}
};
list.addMouseListener(mouseListener);
|
Note that in this example the JList variable is final
because it's referred to by the anonymous MouseListener class.
Lists with Dynamic Contents
The preceding section was a review that focused on using JList
to display static list models. The JList class itself doesn't provide
any methods for adding or removing list items (from its model) --
in fact, the ListModel interface is read-only. This fact has led
many developers to wonder whether JList can be used to to display
a list whose contents change at runtime.
The answer: A JList can be used to display a list with dynamic
contents. Why? Because, although the ListModel interface is read-only,
the contents and size of the list do not have to be immutable. In
fact, JList uses the ListModel's ListDataListener to monitor changes
to the list. Whenever an element is added, changed, or removed,
ListModel implementations must notify their ListDataListeners. JList's
internal ListDataListener handles the updating of what's displayed.
The DefaultListModel class provides a convenient mutable ListModel
implementation that supports the same API as java.util.Vector.
In fact, the DefaultListModel implementation just delegates most
of its operations to a private Vector object. The only functionality
it adds is to call the ListDataListeners each time the underlying
Vector changes. The DynamicList
example demonstrates how the DefaultListModel can be used.
The preferred way to use a dynamic list in an application is to
bind the code that's updating the list to the ListModel, not to
the JList itself. The JList encourages this practice because the
mutable DefaultListModel API isn't exposed by JList. The advantage
of keeping the JList model and the JList (view) separate is that
one can easily replace the view without disturbing the rest of the
application. Occasionally it is more convenient to wire the model
and view together. One simple way to do this is to create a JList
as shown in this example:
class MutableList extends JList {
MutableList() {
super(new DefaultListModel());
}
DefaultListModel getContents() {
return (DefaultListModel)getModel();
}
} |
An application could use MutableList, as shown in the following
code fragment, or could add additional methods that expose the DefaultListModel
API directly.
mutableList.getContents().addElement("one");
mutableList.getContents().addElement("two");
mutableList.getContents().removeElement("one");
You can find a complete example that demonstrates building a dynamic
JList in the DynamicList.java
example.
ListSelectionModel: Enabling Toggle Selection
Mode
JList delegates all the work of managing the selection, as well
as all the work of implementing selection modes such as single or
interval selection, to an object that implements the ListSelectionModel
interface. This object is the value of the JList selectionModel
property. The ListUI look-and-feel implementations map mouse and
keyboard input to ListSelectionModel changes. Then the ListUI responds
to these changes by updating the display as necessary. For example,
each mouse press or mouse drag event causes a selection model update
similar to this:
list.getSelectionModel().setSelectedInterval(index, index);
-- where index is the index of the cell under the
mouse.
By default, selecting a JList entry that's already selected doesn't
do anything. Sometimes the behavior you want in this situation is
for the selection to toggle -- that is, if the entry isn't selected
already, to become selected, or if it is selected, for the selection
to be cleared. To enable this kind of functionality, you can replace
the default list selection model with one that overrides setSelectedInterval()
and toggles the selected state. Here's the class:
class ToggleSelectionModel extends DefaultListSelectionModel
{
public void setSelectionInterval(int index0, int index1) {
if (isSelectedIndex(index0)) {
super.removeSelectionInterval(index0, index1);
}
else {
super.setSelectionInterval(index0, index1);
}
}
} |
To use our new selection model, we just set the corresponding
JList property, in this fashion:
JList list = new JList(JComponent.class.getMethods());
list.setSelectionModel(new ToggleSelectionModel());
NOTE: In the preceding example, as in many of the other
examples presented in this article, we're just using the list
of methods in JComponent as the array of objects to display in
the JList.
Try out the example we have presented, and you'll see that the
ToggleSelectionModel class does its basic job just fine, except
for one small flaw: dragging the mouse around within a list entry
causes the selection to flicker. That's because each time the mouse
moves (while dragging), the selection is redundantly updated (as
shown earlier). Normally, the selection model ignores the redundant
updates -- but, with our new selection model, they're not redundant
any more.
To make all this work properly, we'll change the ToggleSelectionModel
so that toggling only occurs for the first update generated by a
mouse press-drag-release gesture. The ListSelectionModel's valueIsAdjusting
property is true while a while a mouse gesture is under way. When
the mouse button is released, the property is reset to false. We
use the valueIsAdjusting property to ensure that toggling
is enabled only for the first selection model update in a gesture:
class ToggleSelectionModel extends DefaultListSelectionModel
{
boolean gestureStarted = false;
public void setSelectionInterval(int index0, int index1) {
if (isSelectedIndex(index0) && !gestureStarted) {
super.removeSelectionInterval(index0, index1);
}
else {
super.setSelectionInterval(index0, index1);
}
gestureStarted = true;
}
public void setValueIsAdjusting(boolean isAdjusting) {
if (isAdjusting == false) {
gestureStarted = false;
}
}
} |
You can find a complete example that demonstrates the ToggleSelectionModel
in the ToggleSelection.java
example.
ListSelectionModel: Tracking the Selection
The ListSelectionModel API was designed to accommodate selections
that contain large sets of indices. For example, in a list that
has a thousand elements, every third element might be selected.
When the set of selected indices changes, only a simple notification
is provided. The ListSelectionListener.valueChanged()
method is applied to a ListSelectionEvent that just
records the first and last selection indices that changed. If the
current selection encompasses List Indices 2 through 6, and the
selection is changed to Indices 4 through 10, then the ListSelectionEvent
reports that there has been a change between indices 2 and 10.
Clearly, this is only a rough characterization of what has actually
happened. A complete characterization would report which indices
were deselected, which ones were newly selected, and which ones
didn't change. The ListSelectionModel doesn't provide a complete
characterization, because doing so would be too expensive (and usually
unnecessary).
This optimization can make some applications more difficult to
write. If an application creates a JList whose selection models
selection mode is SINGLE_SELECTION -- that is, a JList
in which only one item can be selected at a time -- then the application
might want to perform some special action when an item is deselected
or selected. In other words, each time the selection changes, you
might need to take some action on behalf of the object that has
been selected, and then on the object that has been deselected.
In SINGLE_SELECTION mode, you can work this out because
ListSelectionEvent firstIndex and lastIndex
properties will always represent the previously and newly selected
indices. However, this approach doesn't scale to other selection
modes because the notification is post facto. A cleaner way
to track the selection is to replace the selection model with one
that provides the kind of notification you need. For example, here's
a single selection model that calls a new method each time the selection
changes:
class SingleSelectionModel extends DefaultListSelectionModel {
public SingleSelectionModel() {
setSelectionMode(SINGLE_SELECTION);
}
public void setSelectionInterval(int index0, int index1) {
int oldIndex = getMinSelectionIndex();
super.setSelectionInterval(index0, index1);
int newIndex = getMinSelectionIndex();
if (oldIndex != newIndex) {
updateSingleSelection(oldIndex, newIndex);
}
}
public void updateSingleSelection(int oldIndex, int newIndex) {
}
} |
A convenient way to use SingleSelectionModel is to create an anonymous
subclass that overrides updateSingleSelection() to
call some application specific handler. For example:
ListSelectionModel selectionModel = new SingleSelectionModel() {
public void updateSingleSelection(int oldIndex, int newIndex) {
System.out.println("Index was " + oldIndex + " is " + newIndex);
}
};
JList list = new JList();
list.setSelectionModel(selectionModel);
|
You can find a complete example that demonstrates the SingleSelectionModel
in the SingleSelection.java
example.
JList Performance: Fixed Size Cells, Fast
Renderers
If you configure a JList with thousands of entries, or with 50
or 60 visible entries, you may find that your app's performance
isn't quite adequate, particularly on slower machines. You're likely
to encounter a more pronounced version of the same problem with
JTable, because it uses the same overall painting strategy as JList
and allows your application to put more cells on the screen than
JList does. This section discusses a few simple remedies for this
kind of problem. The approach is specific to JList; however, the
lightweight cell renderer could just as easily be applied to JTable.
By default, JList assumes that list cells vary in size. Since
the height of each row may vary, the preferred size of the list
is the sum of all the preferred cell heights and the maximum of
the preferred cell widths. Although the JList implementation does
cache information about the layout, there's a cost in both time
and space for managing this general case by default. Assuming that
one is using the default cell renderer for lists that contain hundreds
of items, this overhead isn't worth worrying about. For large lists
of fixed size cells, it's worth using the following JList properties
to configure the layout of the list directly:
fixedCellWidth - Force all cells to be the same width
fixedCellHeight - Force all cells to be the same height
prototypeCellValue - Compute fixedCellWidth, fixedCellHeight
The first two properties allow the developer to specify that all
of cells have the same width and/or height. A value of -1
means that the dimension must be computed as described above. Note
that these dimensions define the preferred size of the JList, less
the space allocated for the border (see JList.getInsets()).
In many cases, providing reasonable values for fixedCellWidth
and fixedCellHeight is messy because those values depend
on the cell renderer and the current font and so on. The prototypeCellValue
property can be used instead. If the prototypeCellValue is
non-null, it's used to compute fixedCellWidth and fixedCellHeight
by configuring the cellRenderer (at index equals zero) for the specified
value and then computing the renderer components preferred size.
By choosing a big prototypeCellValue -- that is, one that
will cause the renderer components preferred size to be as large
as needed -- the developer can effectively set the fixed cell size
properties. One common idiom is to use the prototypeCellValue
to define fixedCellHeight and then to define the preferred
width of the list by setting fixedCellWidth:
list1.setPrototypeCellValue("123-45-6789");
list1.setFixedCellWidth(200);
This idiom is useful if you need to force the list to line up
in a column with something else. NOTE: be sure to set the
prototypeCellValue property after setting the cell
renderer (see setCellRenderer()).
One can make modest improvements in scrolling performance by building
a custom cell renderer and by reducing the cost of transforming
list model elements to displayable strings. Note that the optimizations
discussed below trade off generality for speed: they shouldn't be
applied unless performance is critical.
You can find a complete example that demonstrates the cell renderer
optimizations described in this section in the
FastRenderer.java
example .
The model displayed by the JList components in our example is
just an array of Method objects; it's effectively generated like
this:
ListModel model = new AbstractListModel() {
private final Method[] methods = JComponent.class.getMethods();
public int getSize() { return methods.length; }
public Object getElementAt(int i) { return methods[i]; }
};
When model elements are used for display or to compute the list's
preferred width, the default list cell renderer converts them to
strings with Object.toString(). This transformation
is expensive. But it can be avoided -- if the list model is only
being used to drive the display -- by converting the array of Method
objects to an array of Strings. Here's an example of the same model,
with the string conversion wired in:
ListModel model = new AbstractListModel() {
private String[] getMethodNames() {
Method[] methods = JComponent.class.getMethods();
String[] names = new String[methods.length];
for(int i = 0; i < methods.length; i++) {
names[i] = methods[i].toString();
}
return names;
}
private final String[] names = getMethodNames();
public int getSize() { return names.length; }
public Object getElementAt(int i) { return names[i]; }
};
|
The DefaultListCell renderer class uses a JLabel
to render each list item. The JLabel class centers and left-justifies
its text by default, and it clips the text and appends an ellipsis
("...") if the space in which it's to be drawn isn't wide enough.
Sometimes, just left justifying and centering the text is enough.
The cell renderer in the
FastRenderer.java example just caches the text
offset and baseline and paints the text with a minimum of overhead.
The FastRenderer application is a simple benchmark that compares
the benefit of using a custom cell renderer to that of using the
default one. The improvement ranges from 20 to 40 percent, depending
on the platform.
|