JLists, Data Models, and Cell Renderers

1. Introduction: Custom Models and Renderers

1.1 Using Custom Data Models

With almost any Swing component, you can separate the underlying data structure containing the data from the GUI control that displays the data. It is rare to want to do that with simple controls like buttons, but with lists, tables, and trees, it is inconvenient to have to copy data out of an existing hash table, array, linked list, or other structure into the format needed by the GUI control. So Swing lets you use your existing structure directly. You simply implement an interface that tells Swing how to access the data, and then use the data directly in the component.

1.2. Using a Custom Renderer

Swing has a few simple defaults for displaying values in lists, trees, and tables. In a 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.

1.3. Main Swing Components That Use Models and Renderers

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.

2. JList with Fixed Set of Choices

2.1. Building JList: Pass Strings to JList Constructor

The simplest way to use a 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);

2.2. Setting Visible Rows

You set the number of rows via the 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);

2.3. Handling Events

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);
      }
    }
  }
}

2.4 Simple JList Example: Source Code

Here's a JList that displays the last selection in a JTextField.

JListSimpleExample.java (Download source code)

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.

2.5. Simple JList Example: Result

Simple JList

3. JList with Changeable Choices

3.1. Building JList: Pass DefaultListModel to JList Constructor. Add/remove choices to this DefaultListModel.

First create a 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.

3.2. Setting Visible Rows

Same as in previous example. Use setVisibleRowCount and drop JList in a JScrollPane.

3.3. Handling Events

Same as in previous example. Attach a ListSelectionListener.

3.4. Adding Options to JList at Run-Time

You add/remove entries by calling the same methods on the 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.

3.5. Modifiable JList: Source Code

Following is a JList made using a DefaultListModel. Pressing the button adds an entry to the list.

DefaultListModelExample.java (Download source code)

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.

3.6. Modifiable JList: Result

JList with DefaultListModel

4. JList with Custom Data Model

4.1. Idea

Instead of predetermining the data structure that holds list elements, Swing lets you use your own data structure as long as you tell it how to get the data into or out of that structure. Here, I have a collection of type 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.

4.2. ListModel Interface

To be a ListModel, you must implement the following four methods:

4.3 Custom Model: Source Code

JListCustomModel.java (Download source code)

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.

JavaLocationListModel.java (Download source code)

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) {}
}

JavaLocationCollection.java (Download source code)

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.

JavaLocation.java (Download source code)

/** 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;
  }
}

4.4. Custom Model: Result

JList with Custom ListModel

5. JList with Custom Renderer

5.1. Idea

Instead of predetermining how it will draw list elements, Swing lets you specify what graphical component to use for various entries. Normally Swing uses a 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.

5.2. The getListCellRendererComponent Method

The only method in the ListCellRenderer interface is getListCellRendererComponent. It takes the following args, returning a Component:

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.

5.3. Custom Renderer: Source Code

JListCustomRenderer.java (Download source code)

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.

JavaLocationRenderer.java (Download source code)

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);
  }
}

5.4 Custom Renderer: Result

JList with Custom Renderer


This page is part of my Quick Swing Tutorial for AWT Programmers. © 1999 Marty Hall. All source code freely available for unrestricted use. Created for for work in the Research and Technology Development Center of the Johns Hopkins University Applied Physics Lab, for courses in the Johns Hopkins Part-Time MS Program in Computer Science, and for various industry seminars and Java short courses.