Create a Collections-Aware JComboBox





Create a Collections-Aware JComboBox

You've moved on from Vector; your combo boxes should, too.

JComboBox is one of Swing's oldest components. Unfortunately, it accepts arrays of objects and Vectors only. Now that Collections objects like List have been part of the JDK for years, it would be nice to use them directly in a combo box without shuffling objects in and out of arrays. Fortunately, the JComboBox uses an MVC (Model-View-Controller) architecture, so you can solve this problem with a simple implementation of a ComboBoxModel.

To start, you need to figure out what the custom model should do. For our purposes, it needs to accept a List in the constructor and preserve any ordering supplied. Another nifty feature would be automatic updates. If you add or delete values to the List, the combo box should update itself automatically. Figure is a good start.

A basic combo box to accept lists
public class ListComboBoxModel implements ComboBoxModel {
	protected List data;
	
	public ListComboBoxModel(List list) {
		this.listeners = new ArrayList();
		this.data = list;
		if(list.size() > 0) {
			selected = list.get(0);
		}
	}
	
	protected Object selected;
	public void setSelectedItem(Object item) {
		this.selected = item;
	}
	public Object getSelectedItem() {
		return this.selected;
	}

	public Object getElementAt(int index) {
		return data.get(index);
	}
	public int getSize() {
		return data.size();
	}

	protected List listeners;
	public void addListDataListener(ListDataListener l) {
		listeners.add(l);
	}
	public void removeListDataListener(ListDataListener l) {
		this.listeners.remove(l);
	}
}

This implementation is pretty much what you'd expect. Each method in ComboBoxModel is implemented (along with its parent interface, ListDataModel). The constructor saves a reference to the List and selects the first element if there is one. The selectedItem accessor works as expected, using the selected variable. getElementAt() and getSize() both pass the work on to the underlying List, and the ListDataListener methods work with a second List for managing the listeners. The important thing to notice here is that the code saves the reference to the List that was passed in, rather than creating a copy. This means that the model will always be in sync with the underlying list implementation. If you call list.add("new item"), it will show up in the combo box automatically.

To test this, use the simple class in Figure.

Testing the List-based JComboBox
public class CBTest {
	public static void main(String[] args) {
		JFrame frame = new JFrame("Hack #4: Create a Collections-Aware
					JComboBox");
		Container root = frame.getContentPane();
		root.setLayout(new BoxLayout(root,BoxLayout.X_AXIS));

		// List combo box
		final List list = new ArrayList();
		list.add("Blinky");
		list.add("Pinky");
		list.add("Inky");

		final ListComboBoxModel mod2 = new ListComboBoxModel(list);
		JComboBox cb2 = new JComboBox();
		cb2.setModel(mod2);
		root.add(cb2);

		final JButton bt2 = new JButton("Add Item");
		bt2.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent evt) {
				list.add("Clyde");

			}
		});
		root.add(bt2);
		// show the frame
		frame.pack();
		frame.setVisible(true);

	}
}

The program creates a JComboBox that uses the new ListComboBoxModel. First, it creates a list, populates it with data, passes it to the ListComboBoxModel constructor, and then sends that to the new JComboBox(). There is also a button that adds a new item to the list when clicked.

When you compile and run this program, it…doesn't work! The addition to the List doesn't show up in the combo box. A look over the API might remind you of the ListDataListener class. When setModel() is called, the JComboBox registers itself as a listener so that it can update itself when the model changes. This means the ListComboBoxModel needs to fire off an event when the underlying List changes.

The problem here is that Java doesn't provide a standard event mechanism for collections. No problemwe can write our own. Because ActionEvents are the most common ones in Swing, just reuse those with a command string of "update". Here's the new event handling code added to the bottom of ListComboBoxModel:

public class ListComboBoxModel implements ComboBoxModel, ActionListener {
	//..... the rest of the code

	// event code
		public void actionPerformed(ActionEvent evt) {
			if(evt.getActionCommand().equals("update")) {
				this.fireUpdate();
			}
		}

		public void fireUpdate() {
			ListDataEvent le = new ListDataEvent(this,
				ListDataEvent.CONTENTS_CHANGED,
				0,
				data.size());
			for(int i=0; i<listeners.size(); i++) {
				ListDataListener l = (ListDataListener)listeners.get(i);
				l.contentsChanged(le);
			}
		}

The actionPerformed() method implements ActionListener. It just looks for events with the "update" command and calls fireUpdate(). That sends a ListDataEvent to all of the model's listeners, which includes the JComboBox itself.

Here is the modified JButton from the sample program:

bt2.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent evt) {
		list.add("Clyde");
		mod2.actionPerformed(new ActionEvent(bt2,0,"update"));
	}
});

Running the program again, everything works as expected, as seen in Figure. When you press the button, Clyde is added to the list and the combo box updates itself.

The collections-aware combo box


Because the List is backing the new model, you have to consider it to be live. This means you have to address any changes that need to be done on the event-dispatch thread in order to avoid threading issues (like race conditions). In this program, the code modifies the List from another action listener, which means the code is already on the event thread; however, if this was not the case, you would have to use another mechanism, such as SwingUtilities.invokeLater().


Now that you have a combo box that's aware of Lists, it makes sense to add another that understands Maps. Many times when you create a UI, you will want the user to select from a set of values. These values are very meaningful to your program, but because they often come from a database, they are short strings like "Calc_Rng", which won't mean anything to your users. They expect to see something like Calculate Range. What we need is a simple structure to map between the user-friendly descriptions and the real values. Sounds like a job for Map (Dora fans unite)!

Because Map is a collection, the implementation will be similar to what you've already seen; in fact, you can build it with a subclass of ListComboBoxModel. There are a few issues to tackle first, though. A Map defines a set of mappings between keys and values; it does not define the order of the keys themselves. This will make getElementAt(index) hard to implement because there is no notion of order in Maps. Further, the combo box only knows about the keys it uses for display, and not the underlying values, so you will need another way of pulling the real values out of the model. With these issues in mind, take a look at Figure.

Map-based combo box model
public class MapComboBoxModel extends ListComboBoxModel {

	protected Map map_data;
	protected List index;

	public MapComboBoxModel(Map map) {
		this.map_data = map;

index = new ArrayList();
		buildIndex();
		if(index.size() > 0) {
			selected = index.get(0);
		}
	}

	protected void buildIndex() {
		index = new ArrayList(map_data.keySet());
	}

	public Object getElementAt(int i) {
		return index.get(i);
	}

	public int getSize() {
		return map_data.size();
	}

	public void actionPerformed(ActionEvent evt) {
		if(evt.getActionCommand().equals("update")) {
			buildIndex();
			fireUpdate();

		}
	}

	public Object getValue(Object selectedItem) {
		return map_data.get(selectedItem);
	}
	public Object getValue(int selectedItem) {
		return getValue(index.get(selectedItem));
	}
}

The MapComboBoxModel accepts a collection in its constructorthis time a Mapsaving it for later reference. To maintain the order of the keys, the class uses a List called index. The constructor calls buildIndex() to populate the List with the Map's set of keys, and then sets the selected itemjust like in the List version. getElementAt() uses the index to get the display values and getSize() uses the size of the Map itself.

actionPerformed() is different from the List version and calls buildIndex() before fireUpdate(). This ensures that the index is always in sync with the underlying map and that the JComboBox reflects that. There is no implementation of fireUpdate() or managing the listeners because the parent class, ListComboBoxModel, takes care of those.

The final additions are the two getValue() methods, which allow you to retrieve the actual values out of the Map, based on an index or key. One uses the actual selected item and the other uses the index returned by JComboBox. getSelectedIndex().

Here's a slight modification to the test program to try this out:

	// Map Combo Box
	final Map map = new HashMap();
	map.put("Red", "#ff0000");
	map.put("Green", "#00ff00");
	map.put("Blue", "#0000ff");

	final MapComboBoxModel mod3 = new MapComboBoxModel(map);
	final JComboBox cb3 = new JComboBox();
	cb3.setModel(mod3);
	root.add(cb3);
	final JButton bt3 = new JButton("Test Selection");
	bt3.addActionListener(new ActionListener() {
		public void actionPerformed(ActionEvent evt) {
			System.out.println("Human color: " + cb3.getSelectedItem());
			System.out.println("Computer color: " +
				mod3.getValue(cb3.getSelectedIndex()));
		}
	});
	root.add(bt3);

This HashMap maps human-readable color names into the hex values that my program wants. The associated button will test the currently selected color, printing both the description the user sees and the underlying hex value.

Again, you would have to send an ActionEvent to the model to keep it in sync if you added new elements.


The one downside to this approach is that you have no control over the order of the items displayed to the user. It depends on how the Map decides to store them. To impose order on them, you could sort the index in the buildIndex method (e.g., alphabetically), but I think I'll leave that as a future enhancement.


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows