[an error occurred while processing this directive]

Page One
Page Two
The Databank
What Is Swing?
Special Report
IDE Roundup
Swing and the Web
Swing Text
Tech Topics
Friends
Tips and Tricks
The PLAF Papers
Call 911
The Archive
JFC Home
Download Swing
Swing API Docs
Download JDK
JDK Docs
Java Tutorial
 
The Swing Connection Tech Topics

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.

 

[an error occurred while processing this directive]