|
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 ReviewIn 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); ScrollingJList 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); SelectionBy 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 String[] data = {"one", "two", "free", "four"}; JList dataList = new JList(data); dataList.setSelectedIndex(1); // select "two" dataList.getSelectedValue(); // returns "two" Lists with Dynamic ContentsThe 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 Simple dynamic-content JList applications can use the // 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, fixedCellHeightWe 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 RenderersJList uses a 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 HandlingJList doesn't provide any special support for handling double or
triple (or n) mouse clicks; however, it's easy to handle
them using a 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 ContentsThe 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 ModeJList 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 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());
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 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 In 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 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 RenderersIf 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 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 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 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.
|