| JLists, Data Models, and Cell Renderers |
|---|
JList, values that are
Icons are drawn directly, while other objects are
converted to strings via their toString method, then
displayed via a JLabel. However, Swing also lets you
define arbitrary mappings between values and display components,
yielding whatever visual representation you want for values of
specified types. This is done by building a "cell renderer"
that takes the containing component, the value of the entry, a few
state parameters that say things like whether or not the value is
currently selected, and then returns a Component that is
used to draw the value.
JList, JTable, and JTree are
the components that most commonly make use of custom models or
renderers. The following section illustrates various uses of
JList, with similar principles applicable to
JTable and JTree. First, it illustrates
passing data directly to the JList. Second, it shows a
JList using a model explicitly, but making use of a
built-in one. Third it defines a custom model and uses that in the
Jlist. Finally it defines a custom renderer.
JList is to supply an array of
strings to the JList constructor. Unlike an AWT
List, JList does not have a way to directly
add or remove elements once the JList is created. For
that, you have to use a ListModel (discussed in the next
section). But the approach here is easier in the common case of displaying
a fixed set of choices.
String options = { "Option 1", ... , "Option N"};
JList optionList = new JList(options);
setVisibleRowCount
method. However, this is not useful unless the JList has
scrollbars. As in all of Swing, scrollbars are supported only by
dropping the component in a JScrollPane.
optionList.setVisibleRowCount(4); JScrollPane optionPane = new JScrollPane(optionList); someContainer.add(optionPane);
JLists generate ListSelection events, so you
attach a ListSelectionListener, which uses the
valueChanged method. The one thing to note is that a
single click generates three events: one for the deselection of
the originally selected entry, one indicating the selection is moving,
and one for the selection of the new entry. In the first two cases,
the ListEvent's getValueIsAdjusting method
returns true, so you typically check if it is
false if you only care about the selection. Of course,
you can also totally ignore events and later look up which item
(getSelectedValue) or index
(getSelectedIndex) is currently selected. If the
JList supports multiple selections (use
setSelectionMode to specify this; default is single
selections), you use getSelectedValues and
getSelectedIndexes to get an array of the
selections.
public class SomeClass {
private JList optionList;
...
public void someMethod() {
....
MyListListener listener = new MyListListener();
optionList.addListSelectionListener(listener);
}
...
private class MyListListener
implements ListSelectionListener {
public void valueChanged(ListSelectionEvent event) {
if (!event.getValueIsAdjusting()) {
String selection =
optionList.getSelectedValue().toString();
doSomethingWith(selection);
}
}
}
}
JList that displays the last selection in a
JTextField.
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.border.*;
public class JListSimpleExample extends JFrame {
public static void main(String[] args) {
new JListSimpleExample();
}
private JList sampleJList;
private JTextField valueField;
public JListSimpleExample() {
super("Creating a Simple JList");
WindowUtilities.setNativeLookAndFeel();
addWindowListener(new ExitListener());
Container content = getContentPane();
// Create the JList, set the number of visible rows, add a
// listener, and put it in a JScrollPane.
String[] entries = { "Entry 1", "Entry 2", "Entry 3",
"Entry 4", "Entry 5", "Entry 6" };
sampleJList = new JList(entries);
sampleJList.setVisibleRowCount(4);
Font displayFont = new Font("Serif", Font.BOLD, 18);
sampleJList.setFont(displayFont);
sampleJList.addListSelectionListener(new ValueReporter());
JScrollPane listPane = new JScrollPane(sampleJList);
JPanel listPanel = new JPanel();
listPanel.setBackground(Color.white);
Border listPanelBorder =
BorderFactory.createTitledBorder("Sample JList");
listPanel.setBorder(listPanelBorder);
listPanel.add(listPane);
content.add(listPanel, BorderLayout.CENTER);
JLabel valueLabel = new JLabel("Last Selection:");
valueLabel.setFont(displayFont);
valueField = new JTextField("None", 7);
valueField.setFont(displayFont);
JPanel valuePanel = new JPanel();
valuePanel.setBackground(Color.white);
Border valuePanelBorder =
BorderFactory.createTitledBorder("JList Selection");
valuePanel.setBorder(valuePanelBorder);
valuePanel.add(valueLabel);
valuePanel.add(valueField);
content.add(valuePanel, BorderLayout.SOUTH);
pack();
setVisible(true);
}
private class ValueReporter implements ListSelectionListener {
public void valueChanged(ListSelectionEvent event) {
if (!event.getValueIsAdjusting())
valueField.setText(sampleJList.getSelectedValue().toString());
}
}
}
Note: also requires
WindowUtilities.java
and ExitListener.java,
shown earlier.
DefaultListModel via the empty
constructor. This implements the same methods as does
java.util.Vector, so you can manipulate it just like a
Vector. Then pass this to the JList
constructor. Now, any changes to the items in the
DefaultListModel are reflected in the JList.
setVisibleRowCount and
drop JList in a JScrollPane.
ListSelectionListener.
DefaultListModel as you would on a
Vector. E.g. addElement to insert an option
at the end, remove to delete the item at a specified
index, etc. However, this may change the preferred size of the
JList, which, depending on the layout manager in use,
might require you to invalidate then
validate the window containing the JList,
thus re-running the layout manager.
JList made using a
DefaultListModel. Pressing the button adds an entry to
the list.
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
public class DefaultListModelExample extends JFrame {
public static void main(String[] args) {
new DefaultListModelExample();
}
JList sampleJList;
private DefaultListModel sampleModel;
public DefaultListModelExample() {
super("Creating a Simple JList");
WindowUtilities.setNativeLookAndFeel();
addWindowListener(new ExitListener());
Container content = getContentPane();
String[] entries = { "Entry 1", "Entry 2", "Entry 3",
"Entry 4", "Entry 5", "Entry 6" };
sampleModel = new DefaultListModel();
for(int i=0; i<entries.length; i++)
sampleModel.addElement(entries[i]);
sampleJList = new JList(sampleModel);
sampleJList.setVisibleRowCount(4);
Font displayFont = new Font("Serif", Font.BOLD, 18);
sampleJList.setFont(displayFont);
JScrollPane listPane = new JScrollPane(sampleJList);
JPanel listPanel = new JPanel();
listPanel.setBackground(Color.white);
Border listPanelBorder =
BorderFactory.createTitledBorder("Sample JList");
listPanel.setBorder(listPanelBorder);
listPanel.add(listPane);
content.add(listPanel, BorderLayout.CENTER);
JButton addButton =
new JButton("Add Entry to Bottom of JList");
addButton.setFont(displayFont);
addButton.addActionListener(new ItemAdder());
JPanel buttonPanel = new JPanel();
buttonPanel.setBackground(Color.white);
Border buttonPanelBorder =
BorderFactory.createTitledBorder("Adding Entries");
buttonPanel.setBorder(buttonPanelBorder);
buttonPanel.add(addButton);
content.add(buttonPanel, BorderLayout.SOUTH);
pack();
setVisible(true);
}
private class ItemAdder implements ActionListener {
public void actionPerformed(ActionEvent event) {
int index = sampleModel.getSize();
sampleModel.addElement("Entry " + (index+1));
getContentPane().invalidate();
getContentPane().validate();
sampleJList.setSelectedIndex(index);
sampleJList.ensureIndexIsVisible(index);
}
}
}
Note: also requires
WindowUtilities.java
and ExitListener.java,
shown earlier.
JavaLocation. A
JavaLocation is a class representing information on a
country that contains a city or province named "Java". I
then have a class called JavaLocationCollection that
includes an array of JavaLocations and a separate count
of the number of unique countries contained in the array. Rather than
copying the data out of the JavaLocationCollection, I
simply define a small class called JavaLocationListModel
that tells how to extract data out of that collection. This list model
is then supplied to the JList constructor. Note that, in
this simple case, the data is not modifiable. If you wanted it to be,
the best bet is to start with AbstractListModel instead
of just ListModel. It has some support already built in
for attaching and removing listeners, and some place holder functions
to write yourself.
getElementAtgetSizeaddListDataListenerremoveListDataListener
import java.awt.*;
import javax.swing.*;
public class JListCustomModel extends JFrame {
public static void main(String[] args) {
new JListCustomModel();
}
public JListCustomModel() {
super("JList with a Custom Data Model");
WindowUtilities.setNativeLookAndFeel();
addWindowListener(new ExitListener());
Container content = getContentPane();
JavaLocationCollection collection =
new JavaLocationCollection();
JavaLocationListModel listModel =
new JavaLocationListModel(collection);
JList sampleJList = new JList(listModel);
Font displayFont = new Font("Serif", Font.BOLD, 18);
sampleJList.setFont(displayFont);
content.add(sampleJList);
pack();
setVisible(true);
}
}
Note: also requires
WindowUtilities.java
and ExitListener.java,
shown earlier.
import javax.swing.*;
import javax.swing.event.*;
public class JavaLocationListModel implements ListModel {
private JavaLocationCollection collection;
public JavaLocationListModel(JavaLocationCollection collection) {
this.collection = collection;
}
public Object getElementAt(int index) {
return(collection.getLocations()[index]);
}
public int getSize() {
return(collection.getLocations().length);
}
public void addListDataListener(ListDataListener l) {}
public void removeListDataListener(ListDataListener l) {}
}
public class JavaLocationCollection {
private static JavaLocation[] defaultLocations =
{ new JavaLocation("Belgium",
"near Liege",
"flags/belgium.gif"),
new JavaLocation("Brazil",
"near Salvador",
"flags/brazil.gif"),
new JavaLocation("Colombia",
"near Bogota",
"flags/colombia.gif"),
new JavaLocation("Indonesia",
"main island",
"flags/indonesia.gif"),
new JavaLocation("Jamaica",
"near Spanish Town",
"flags/jamaica.gif"),
new JavaLocation("Mozambique",
"near Sofala",
"flags/mozambique.gif"),
new JavaLocation("Philippines",
"near Quezon City",
"flags/philippines.gif"),
new JavaLocation("Sao Tome",
"near Santa Cruz",
"flags/saotome.gif"),
new JavaLocation("Spain",
"near Viana de Bolo",
"flags/spain.gif"),
new JavaLocation("Suriname",
"near Paramibo",
"flags/suriname.gif"),
new JavaLocation("United States",
"near Montgomery, Alabama",
"flags/usa.gif"),
new JavaLocation("United States",
"near Needles, California",
"flags/usa.gif"),
new JavaLocation("United States",
"near Dallas, Texas",
"flags/usa.gif")
};
private JavaLocation[] locations;
private int numCountries;
public JavaLocationCollection(JavaLocation[] locations) {
this.locations = locations;
this.numCountries = countCountries(locations);
}
public JavaLocationCollection() {
this(defaultLocations);
}
public JavaLocation[] getLocations() {
return(locations);
}
public int getNumCountries() {
return(numCountries);
}
// Assumes the list is sorted by country name
private int countCountries(JavaLocation[] locations) {
int n = 0;
String currentCountry, previousCountry = "None";
for(int i=0;i<locations.length;i++) {
currentCountry = locations[i].getCountry();
if(!previousCountry.equals(currentCountry))
n = n + 1;
currentCountry = previousCountry;
}
return(n);
}
}
Note: also requires
various GIF images.
/** Simple data structure with three properties: country,
* comment, and flagFile. All are strings, and they are
* intended to represent a country that has a city or
* province named "Java", a comment about a more
* specific location within the country, and a path
* specifying an image file containing the country's flag.
* Used in examples illustrating custom models and cell
* renderers for JLists.
*
* 1998-99 Marty Hall, http://www.apl.jhu.edu/~hall/java/
*/
public class JavaLocation {
private String country, comment, flagFile;
public JavaLocation(String country, String comment,
String flagFile) {
setCountry(country);
setComment(comment);
setFlagFile(flagFile);
}
/** String representation used in printouts and in JLists */
public String toString() {
return("Java, " + getCountry() + " (" + getComment() + ").");
}
/** Return country containing city or province named "Java". */
public String getCountry() {
return(country);
}
/** Specify country containing city or province named "Java". */
public void setCountry(String country) {
this.country = country;
}
/** Return comment about city or province named "Java".
* Usually of the form "near such and such a city".
*/
public String getComment() {
return(comment);
}
/** Specify comment about city or province named "Java". */
public void setComment(String comment) {
this.comment = comment;
}
/** Return path to image file of country flag. */
public String getFlagFile() {
return(flagFile);
}
/** Specify path to image file of country flag. */
public void setFlagFile(String flagFile) {
this.flagFile = flagFile;
}
}
JLabel, either directly
from the String representation of the entry or directly
from the value if the value is an Icon. But in this
example, which continues the previous one on representing countries
with locations named "Java", I want to include an image of
the flag of the country for each JavaLocation. So I
define a class that implements the ListCellRender
interface, with a method getListCellRendererComponent
that constructs the Component of interest given the list
entry. I then associate that renderer with the JList via
the JList's setCellRenderer method.
ListCellRenderer interface is
getListCellRendererComponent. It takes the following
args, returning a Component:
JList list
Object value
int index
boolean isSelected
boolean cellHasFocus
Rather than trying to explicitly determine how to color the component
if it is selected, has focus, and so forth, it is often easier to
extend DefaultListCellRenderer, call
super.getListCellRendererComponent (which returns a
JList), then work with it. For example, I keep the
existing text, fg color, bg color, and border, and just add an
icon. Note that this method can be called many times, so you should
cache the new components you generate if at all possible. In the
following example I do this by maintaining a hash table that
associates ImageIcons with JavaLocations. So
if I've seen the JavaLocation before, I reuse the old
ImageIcon.
import java.awt.*;
import javax.swing.*;
public class JListCustomRenderer extends JFrame {
public static void main(String[] args) {
new JListCustomRenderer();
}
public JListCustomRenderer() {
super("JList with a Custom Cell Renderer");
WindowUtilities.setNativeLookAndFeel();
addWindowListener(new ExitListener());
Container content = getContentPane();
JavaLocationCollection collection =
new JavaLocationCollection();
JavaLocationListModel listModel =
new JavaLocationListModel(collection);
JList sampleJList = new JList(listModel);
sampleJList.setCellRenderer(new JavaLocationRenderer());
Font displayFont = new Font("Serif", Font.BOLD, 18);
sampleJList.setFont(displayFont);
content.add(sampleJList);
pack();
setVisible(true);
}
}
Note: also requires
JavaLocationListModel.java,
JavaLocationCollection.java,
JavaLocation.java,
various GIF images,
WindowUtilities.java,
and ExitListener.java,
shown earlier.
import javax.swing.*;
import java.awt.*;
import java.util.*;
public class JavaLocationRenderer extends DefaultListCellRenderer {
private Hashtable iconTable = new Hashtable();
public Component getListCellRendererComponent(JList list,
Object value,
int index,
boolean isSelected,
boolean hasFocus) {
JLabel label =
(JLabel)super.getListCellRendererComponent(list,
value,
index,
isSelected,
hasFocus);
if (value instanceof JavaLocation) {
JavaLocation location = (JavaLocation)value;
ImageIcon icon = (ImageIcon)iconTable.get(value);
if (icon == null) {
icon = new ImageIcon(location.getFlagFile());
iconTable.put(value, icon);
}
label.setIcon(icon);
} else {
// Clear old icon; needed in 1st release of JDK 1.2
label.setIcon(null);
}
return(label);
}
}