Chapter 9. Combo Boxes: 9.1 Jcombobox
Chapter 9. Combo Boxes: 9.1 Jcombobox
Combo Boxes
In this chapter:
JComboBox
Custom editing
9.1 JCombobox
class javax.swing.JComboBox
This class represents a basic GUI component which consists of two parts:
A popup menu (an implementation of javax.swing.plaf.basic.ComboPopup ). By
default this is a JPopupMenu sub-class (javax.swing.plaf.basic.BasicComboPopup )
containing a JList in a JScrollPane.
A button acting as a container for an editor or renderer component, and an arrow
button used to display the popup menu.
The JList uses a ListSelectionModel (see chapter 10) allowing SINGLE_SELECTION
only. Apart from this, JComboBox directly uses only one model, a ComboBoxModel, which
manages data in its JList.
A number of constructors are available to build a JComboBox. The default constructor
can be used to create a combo box with an empty list, or we can pass data to a
constructor as a one-dimensional array, a Vector, or as an implementation of the
ComboBoxModelinterface (see below). The last variant allows maximum control over the
properties and appearance of a JComboBox, as we will see.
As other complex Swing components, JComboBox allows a customizable renderer for
displaying each item in its drop-down list (by default a JLabel sub-class implementation
of ListCellRenderer), and a customizable editor to be used as the combo boxs data
entry component (by default an instance of ComboBoxEditor which uses a JTextField).
We can use the existing default implementations of ListCellRenderer and
ComboBoxEditor, or we can create our own according to our particular needs (which we
will see later in ths chapter). Note that unless we use a custom renderer, the default
renderer will display each element as a String defined by that objects toString()
method (the only exceptions to this are Icon implementations which will be renderered
as they would be in any JLabel). Also note that a renderer returns a Component, but that
component is not interactive and is only used for display purposes (i.e. it acts as a
rubber stampAPI). For instance, if a JCheckBox is used as a renderer we will not be able
to check and uncheck it. Editors, however, are fully interactive.
1
Similar to JList (next chapter), this class uses ListDataEvents to deliver information
about changes in the state of its drop-down lists model. ItemEvents and ActionEvents
are fired when the current selection changes (from any source--programmatic or direct
user input). Correspondingly, we can attach ItemListeners and ActionListeners to
receive these events.
The drop-down list of a JComboBox is a popup menu containing a JList (this is actually
defined in the UI delegate, not the component itself) and can be programmatically
displayed using the showPopup() and hidePopup() methods. As any other Swing popup
menu (which we will discuss in chapter 12), it can be displayed as either heavyweight
or lightweight. JComboBox provides the setLightWeightPopupEnabled() method allowing
us to choose between these modes.
JComboBox also defines an inner interface called KeySelectionManager that declares one
method, selectionForKey(char aKey, ComboBoxModel aModel) , which should be
overriden to return the index of the list element to select when the list is visible (popup
is showing) and the given keyboard character is pressed.
The JComboBox UI delegate represents JComboBox graphically by a container with a
button which encapsulates an arrow button and either a renderer displaying the
currently selected item, or an editor allowing changes to be made to the currently
selected item. The arrow button is displayed on the right of the renderer/editor and will
show the popup menu containing the drop-down list when clicked.
Note: Because of the JComboBox UI delegate construction, setting the border of a JComboBox
does not have the expected effect. Try this and you will see that the container containing
the main JComboBox button gets the assigned border, when in fact we want that button to
recieve the border. There is no easy way to set the border of this button without
customizing the UI delegate, and we hope to see this limitation disappear in a future
version.
When a JComboBox is editable (which it is not by default) the editor component will allow
modification of the currenly selected item. The default editor will appear as a
JTextField accepting input. This text field has an ActionListener attached that will
accept an edit and change the selected item accoringly when/if the Enter key is
pressed. If the focus changes while editing, all editing will be cancelled and a change
will not be made to the selected item.
JComboBox can be made editable with its setEditable() method, and we can specify a
custom ComboBoxEditor with JComboBoxs setEditor() method.. Setting the editable
property to true causes the UI delegate to replace the renderer component in the
button to the specified editor component. Similarly, setting this property to false
9.1.3 DefaultComboBoxModel
class javax.swing.DefaultComboBoxModel
This class represents the default model used by JComboBox, and implements
MutableComboBoxModel. To programmatically select an item we can call its
setSelectedItem() method. Calling this method, as well as any of the
MutableComboBoxModel methods mentioned above, will cause a ListDataEvent to be
fired. To capture these events we can attatch ListDataListeners with
DefaultComboBoxModels addListDataListener() method. We can also remove these
listeners with its removeListDataListener() method.
9.1.5. DefaultListCellRenderer
class javax.swing.DefaultListCellRenderer
This is the concrete implementation of the ListCellRenderer interface used by JList
by default (and this by JComboBoxs JList). This class extends JLabel and its
getListCellRenderer() method returns a this reference, renders the given value by
setting its text to the String returned by the values toString() method (unless the
value is an instance of Icon, in which case it will be rendered as it would be in any
JLabel), and uses JList foreground and background colors depending on whether or
3
A single static EmptyBorder instance is used for all cells that do not have the current
focus. This border has top, bottom, left, and right spacing of 1, and unfortunately
cannot be re-assigned.
which are notified when an edit is accepted. In the default editor this occurs when Enter
is pressed while the text field has the focus.
Note: Unfortunately Swing does not provide an easily reusable ComboBoxEditor
implementation, forcing custom implementations to manage all ActionListener and item
selection/modification functionality from scratch (we hope to see this limitation accounted
for in a future Swing release).
UI Guideline : Advice on Usage and Design Usage
Comboboxes and List Boxes are very similar. In fact a Combobox is an Entry Field with a
drop down List Box. Deciding when to use one or another can be difficult. Our advice is to
think about reader output rather than data input. When the reader only needs to see a
single item then a Combobox is the choice. Use a Combobox where a single selection is
made from a collection and for reading purposes it is only necessary to see a single item,
e.g. Currency USD.
Design
There are a number of things which affect the usability of a combobox. Beyond more than a
4
few items, they become unusable unless the data is sorted in some logical fashion e.g.
alphabetical, numerical. When a list gets longer, usability is affected again.Once a list gets
beyond a couple of hundred items, even when sorted, it becomes very slow for the user to
locate specific item in the list. Some implementations have solved this by offering an ability
to type in partial text and the list "jumps" to the best match or partial match item e.g. type
in "ch" and the combobox will jump to "Chevrolet" as in the example in this chapter. You
may like to consider such an enhancement to a JCombobox to improve the usability in
longer lists.
There are a number of graphical considerations too. Like all other data entry fields,
comboboxes should be aligned to fit attractively into a panel. However, this can be
problematic. You must avoid making a combobox which is simply too big for the list items
contained e.g. a combobox for currency code ( typicall USD for U.S. Dollars ) only needs to
be 3 characters long. So don't make it big enough to take 50 characters. It will look
unbalanced. Another problem, is the nature of the list items. If you have 50 items in a list
where most items are around 20 characters but one item is 50 characters long then should
you make the combobox big enough to display the longer one? Well maybe but for most
occasions your display will be unbalanced again. It is probably best to optimise for the more
common length, providing the the longer one still has meaning when read in its truncated
form. One solution to displaying the whole length of a truncated item is to use the tooltip
facility. When the User places the mouse over an item, a tooltip appears with the full length
data.
One thing you must never do is dynamically resize the combobox to fit a varying length
item selection. This will provide alignment problems and may also add a usability problem
because the pull-down button may become a moving target which denies the user the
option to learn its position with directional memory.
Type
String
String
Icon
Vector
Description
Model's name
Company manufacturer
Model's photograph
A collection of model's trims
Type
Description
String
Trim's name
int
Manufacturer's suggested retail price
int
Invoice
price
String
Engine description
Figure 9.1 Dynamically changeable JComboBoxes allowing comparison of car model and trim
information.
<<file figure9-1.gif>>
The Code: ComboBox1.java
see \Chapter9\1
importjava.awt.*;
importjava.awt.event.*;
importjava.util.*;
importjavax.swing.*;
importjavax.swing.border.*;
importjavax.swing.event.*;
publicclassComboBox1extendsJFrame
{
publicComboBox1(){
super("ComboBoxes[CompareCars]");
getContentPane().setLayout(newBorderLayout());
Vectorcars=newVector();
Carmaxima=newCar("Maxima","Nissan",newImageIcon(
"maxima.gif"));
maxima.addTrim("GXE",21499,19658,"3.0LV6190hp");
maxima.addTrim("SE",23499,21118,"3.0LV6190hp");
maxima.addTrim("GLE",26899,24174,"3.0LV6190hp");
cars.addElement(maxima);
Caraccord=newCar("Accord","Honda",newImageIcon(
"accord.gif"));
accord.addTrim("LXSedan",21700,19303,"3.0LV6200hp");
accord.addTrim("EXSedan",24300,21614,"3.0LV6200hp");
cars.addElement(accord);
Carcamry=newCar("Camry","Toyota",newImageIcon(
"camry.gif"));
camry.addTrim("LEV6",21888,19163,"3.0LV6194hp");
camry.addTrim("XLEV6",24998,21884,"3.0LV6194hp");
cars.addElement(camry);
6
Carlumina=newCar("Lumina","Chevrolet",newImageIcon(
"lumina.gif"));
lumina.addTrim("LS",19920,18227,"3.1LV6160hp");
lumina.addTrim("LTZ",20360,18629,"3.8LV6200hp");
cars.addElement(lumina);
Cartaurus=newCar("Taurus","Ford",newImageIcon(
"taurus.gif"));
taurus.addTrim("LS",17445,16110,"3.0LV6145hp");
taurus.addTrim("SE",18445,16826,"3.0LV6145hp");
taurus.addTrim("SHO",29000,26220,"3.4LV8235hp");
cars.addElement(taurus);
Carpassat=newCar("Passat","Volkswagen",newImageIcon(
"passat.gif"));
passat.addTrim("GLSV6",23190,20855,"2.8LV6190hp");
passat.addTrim("GLX",26250,23589,"2.8LV6190hp");
cars.addElement(passat);
getContentPane().setLayout(newGridLayout(1,2,5,3));
CarPanelpl=newCarPanel("BaseModel",cars);
getContentPane().add(pl);
CarPanelpr=newCarPanel("Compareto",cars);
getContentPane().add(pr);
WindowListenerwndCloser=newWindowAdapter(){
publicvoidwindowClosing(WindowEvente){
System.exit(0);
}
};
addWindowListener(wndCloser);
pl.selectCar(maxima);
pr.selectCar(accord);
setResizable(false);
pack();
setVisible(true);
}
publicstaticvoidmain(Stringargv[]){
newComboBox1();
}
}
classCar
{
protectedStringm_name;
protectedStringm_manufacturer;
protectedIconm_img;
protectedVectorm_trims;
publicCar(Stringname,Stringmanufacturer,Iconimg){
m_name=name;
m_manufacturer=manufacturer;
m_img=img;
m_trims=newVector();
}
publicvoidaddTrim(Stringname,intMSRP,intinvoice,
Stringengine){
7
Trimtrim=newTrim(this,name,MSRP,invoice,engine);
m_trims.addElement(trim);
}
publicStringgetName(){returnm_name;}
publicStringgetManufacturer(){returnm_manufacturer;}
publicIcongetIcon(){returnm_img;}
publicVectorgetTrims(){returnm_trims;}
publicStringtoString(){returnm_manufacturer+""+m_name;}
}
classTrim
{
protectedCarm_parent;
protectedStringm_name;
protectedintm_MSRP;
protectedintm_invoice;
protectedStringm_engine;
publicTrim(Carparent,Stringname,intMSRP,intinvoice,
Stringengine){
m_parent=parent;
m_name=name;
m_MSRP=MSRP;
m_invoice=invoice;
m_engine=engine;
}
publicCargetCar(){returnm_parent;}
publicStringgetName(){returnm_name;}
publicintgetMSRP(){returnm_MSRP;}
publicintgetInvoice(){returnm_invoice;}
publicStringgetEngine(){returnm_engine;}
publicStringtoString(){returnm_name;}
}
classCarPanelextendsJPanel
{
protectedJComboBoxm_cbCars;
protectedJComboBoxm_cbTrims;
protectedJLabelm_lblImg;
protectedJLabelm_lblMSRP;
protectedJLabelm_lblInvoice;
protectedJLabelm_lblEngine;
publicCarPanel(Stringtitle,Vectorcars){
super();
setLayout(newBoxLayout(this,BoxLayout.Y_AXIS));
setBorder(newTitledBorder(newEtchedBorder(),title));
JPanelp=newJPanel();
p.add(newJLabel("Model:"));
8
m_cbCars=newJComboBox(cars);
ActionListenerlst=newActionListener(){
publicvoidactionPerformed(ActionEvente){
Carcar=(Car)m_cbCars.getSelectedItem();
if(car!=null)
showCar(car);
}
};
m_cbCars.addActionListener(lst);
p.add(m_cbCars);
add(p);
p=newJPanel();
p.add(newJLabel("Trim:"));
m_cbTrims=newJComboBox();
lst=newActionListener(){
publicvoidactionPerformed(ActionEvente){
Trimtrim=(Trim)m_cbTrims.getSelectedItem();
if(trim!=null)
showTrim(trim);
}
};
m_cbTrims.addActionListener(lst);
p.add(m_cbTrims);
add(p);
p=newJPanel();
m_lblImg=newJLabel();
m_lblImg.setHorizontalAlignment(JLabel.CENTER);
m_lblImg.setPreferredSize(newDimension(140,80));
m_lblImg.setBorder(newBevelBorder(BevelBorder.LOWERED));
p.add(m_lblImg);
add(p);
p=newJPanel();
p.setLayout(newGridLayout(3,2,10,5));
p.add(newJLabel("MSRP:"));
m_lblMSRP=newJLabel();
p.add(m_lblMSRP);
p.add(newJLabel("Invoice:"));
m_lblInvoice=newJLabel();
p.add(m_lblInvoice);
p.add(newJLabel("Engine:"));
m_lblEngine=newJLabel();
p.add(m_lblEngine);
add(p);
}
publicvoidselectCar(Carcar){m_cbCars.setSelectedItem(car);}
publicvoidshowCar(Carcar){
m_lblImg.setIcon(car.getIcon());
if(m_cbTrims.getItemCount()>0)
m_cbTrims.removeAllItems();
Vectorv=car.getTrims();
for(intk=0;k<v.size();k++)
m_cbTrims.addItem(v.elementAt(k));
m_cbTrims.grabFocus();
}
publicvoidshowTrim(Trimtrim){
m_lblMSRP.setText("$"+trim.getMSRP());
m_lblInvoice.setText("$"+trim.getInvoice());
m_lblEngine.setText(trim.getEngine());
}
}
Class ComboBox1
Class ComboBox1extends JFrame to implement the frame container for this example. It
has no instance variables. The constructor of the ComboBox1 class creates a data
collection with car information as listed above. A collection of cars is stored in Vector
cars, and each car, in turn, receives one or more Trim instances. Other than this, the
ComboBox1 constructor doesn't do much. It creates two instances of CarPanel (see
below) and arranges them in a GridLayout. These panels are used to select and display
car information. Finally two cars are initially selected in both panels.
Class Car
Car is a typical data object encapsulating three data fields listed at the beginning of this
section: car name, manufacturer, and image. In addition, it holds the m_trims vector
representing a collection of Trim instances.
Method addTrim() creates a new Trim instance and adds it to the m_trims vector. The
rest of this class implements typical getXX() methods to allow access to the protected
data fields.
Class Trim
Trim encapsulates four data fields listed at the beginning of this section: trim name,
suggested retail price, invoice price, and engine type. In addition, it holds a reference to
the parent Car instance. The rest of this class implements typical getXX() methods to
allow access to the protected data fields.
Class CarPanel
This class extends JPanel to provide the GUI framework for displaying car information.
Six components are declared as instance variables:
JComboBoxm_cbCars: Combo box to select a car model.
JComboBoxm_cbTrims: Combo box to select a car trim for the selected model.
JLabelm_lblImg: Label to display the model's image.
JLabelm_lblMSRP: Label to display the MSRP.
JLabelm_lblInvoice: Label to display the invoice price.
JLabelm_lblEngine: Label to display the engine description.
Two combo boxes are used to select cars and trims respectively. Note that Car and Trim
data objects are used to populate these combo boxes, so the actual displayed text is
determined by their toString() methods. Both combo boxes receive ActionListeners
to handle item selection. Then a Car item is selected, which triggers a call to the
showCar() method described below. Similarly, a selection of a Trim item triggers a call
to the showTrim() method.
10
The rest of the CarPanel constructor builds JLabels to display a car's image and trim
data. Note how layouts are used in this example. A y-oriented BoxLayout creates a
vertical axis used to allign and position all components. The combo boxes and
supplementary labels are encapsulated in horizontal JPanels. JLabel m_lblImg receives
a custom preferred size to reserve enough space for the photo image. This label is
encapsulated in a panel (with its default FlowLayout) to ensure that this component will
be centered over the container's space. The rest of CarPanel is occupied by the six
labels, which are hosted by a 3x2 GridLayout.
Method selectCar() allows us to select a car programmatically from outside this class.
It invokes the setSelectedItem() method on the m_cbCars combo box. Note that this
call will trigger an ActionEvent which will be captured by the proper listener, resulting
in a showCar()call.
Method showCar() updates the car image and updates the m_cbTrims combo box to
display the corresponding trims of the selected model. The (getItemCount() > 0)
condition is necessary because Swing throws an exception if removeAllItems() is
invoked on an empty JComboBox. Finally, focus is transferred to the m_cbTrims
component.
Method showTrim() updates the contents of the labels displaying trim information:
MSRP, invoice price, and engine type.
Running the Code
Figure 9.1 shows the ComboBox1 application displaying two cars simultaneously for
comparison. Note that all initial information is displayed correctly. Try experimenting
with various selections and note how the combo box contents change dynamically.
UI Guideline : Symmetrical Layout
In this example, the design avoids the problem of having to align the different length
comboboxes by using a symmetrical layout. Overall the window has a good balance and
good use of white space, as in turn do each of the bordered panes used for individual car
selections.
We also need to prevent the user from selecting models (e.g. Nissan Maxima above),
since they do not provide complete information about a specific car, and only serve as
separators between sets of trims.
Note: The hierarchical list organization shown here can easily be extended for use in a
JList, and can handle an arbitrary number of levels. We only use two levels in this
example, however, the design does not limit us to this.
Figure 9.2 JComboBox with a custom model and a custom hierarchical rendering scheme.
<<file figure9-2.gif>>
The Code: ComboBox2.java
see \Chapter9\2
//Unchangedcodefromsection9.2
classCarPanelextendsJPanel
{
protectedJComboBoxm_cbCars;
protectedJLabelm_txtModel;
protectedJLabelm_lblImg;
protectedJLabelm_lblMSRP;
protectedJLabelm_lblInvoice;
protectedJLabelm_lblEngine;
publicCarPanel(Stringtitle,Vectorcars){
super();
setLayout(newBoxLayout(this,BoxLayout.Y_AXIS));
setBorder(newTitledBorder(newEtchedBorder(),title));
JPanelp=newJPanel();
m_txtModel=newJLabel("");
m_txtModel.setForeground(Color.black);
p.add(m_txtModel);
add(p);
p=newJPanel();
12
p.add(newJLabel("Car:"));
CarComboBoxModelmodel=newCarComboBoxModel(cars);
m_cbCars=newJComboBox(model);
m_cbCars.setRenderer(newIconComboRenderer());
ActionListenerlst=newActionListener(){
publicvoidactionPerformed(ActionEvente){
ListDatadata=(ListData)m_cbCars.getSelectedItem();
Objectobj=data.getObject();
if(objinstanceofTrim)
showTrim((Trim)obj);
}
};
m_cbCars.addActionListener(lst);
p.add(m_cbCars);
add(p);
//Unchangedcodefromsection9.2
}
publicsynchronizedvoidselectCar(Carcar){
for(intk=0;k<m_cbCars.getItemCount();k++){
ListDataobj=(ListData)m_cbCars.getItemAt(k);
if(obj.getObject()==car){
m_cbCars.setSelectedItem(obj);
break;
}
}
}
publicsynchronizedvoidshowTrim(Trimtrim){
Carcar=trim.getCar();
m_txtModel.setText(car.toString());
m_lblImg.setIcon(car.getIcon());
m_lblMSRP.setText("$"+trim.getMSRP());
m_lblInvoice.setText("$"+trim.getInvoice());
m_lblEngine.setText(trim.getEngine());
}
}
classListData
{
protectedIconm_icon;
protectedintm_index;
protectedbooleanm_selectable;
protectedObjectm_data;
publicListData(Iconicon,intindex,booleanselectable,
Objectdata){
m_icon=icon;
m_index=index;
m_selectable=selectable;
m_data=data;
}
publicIcongetIcon(){returnm_icon;}
publicintgetIndex(){returnm_index;}
publicbooleanisSelectable(){returnm_selectable;}
publicObjectgetObject(){returnm_data;}
13
publicStringtoString(){returnm_data.toString();}
}
classCarComboBoxModelextendsDefaultComboBoxModel
{
publicstaticfinalImageIconICON_CAR=
newImageIcon("car.gif");
publicstaticfinalImageIconICON_TRIM=
newImageIcon("trim.gif");
publicCarComboBoxModel(Vectorcars){
for(intk=0;k<cars.size();k++){
Carcar=(Car)cars.elementAt(k);
addElement(newListData(ICON_CAR,0,false,car));
Vectorv=car.getTrims();
for(inti=0;i<v.size();i++){
Trimtrim=(Trim)v.elementAt(i);
addElement(newListData(ICON_TRIM,1,true,trim));
}
}
}
//Thismethodonlyallowstrimstobeselected
publicvoidsetSelectedItem(Objectitem){
if(iteminstanceofListData){
ListDataldata=(ListData)item;
if(!ldata.isSelectable()){
ObjectnewItem=null;
intindex=getIndexOf(item);
for(intk=index+1;k<getSize();k++){
Objectitem1=getElementAt(k);
if(item1instanceofListData){
ListDataldata1=(ListData)item1;
if(!ldata1.isSelectable())
continue;
}
newItem=item1;
break;
}
if(newItem==null)
return;//Selectionfailed
item=newItem;
}
}
super.setSelectedItem(item);
}
}
classIconComboRendererextendsJLabelimplementsListCellRenderer
{
publicstaticfinalintOFFSET=16;
protectedColorm_textSelectionColor=Color.white;
protectedColorm_textNonSelectionColor=Color.black;
protectedColorm_textNonselectableColor=Color.gray;
protectedColorm_bkSelectionColor=newColor(0,0,128);
protectedColorm_bkNonSelectionColor=Color.white;
protectedColorm_borderSelectionColor=Color.yellow;
14
protectedColorm_textColor;
protectedColorm_bkColor;
protectedbooleanm_hasFocus;
protectedBorder[]m_borders;
publicIconComboRenderer(){
super();
m_textColor=m_textNonSelectionColor;
m_bkColor=m_bkNonSelectionColor;
m_borders=newBorder[20];
for(intk=0;k<m_borders.length;k++)
m_borders[k]=newEmptyBorder(0,OFFSET*k,0,0);
setOpaque(false);
}
publicComponentgetListCellRendererComponent(JListlist,
Objectobj,introw,booleansel,booleanhasFocus){
if(obj==null)
returnthis;
setText(obj.toString());
booleanselectable=true;
if(objinstanceofListData){
ListDataldata=(ListData)obj;
selectable=ldata.isSelectable();
setIcon(ldata.getIcon());
intindex=0;
if(row>=0)//nooffsetforeditor(row=1)
index=ldata.getIndex();
Borderb=(index<m_borders.length?m_borders[index]:
newEmptyBorder(0,OFFSET*index,0,0));
setBorder(b);
}
else
setIcon(null);
setFont(list.getFont());
m_textColor=(sel?m_textSelectionColor:
(selectable?m_textNonSelectionColor:
m_textNonselectableColor));
m_bkColor=(sel?m_bkSelectionColor:
m_bkNonSelectionColor);
m_hasFocus=hasFocus;
returnthis;
}
publicvoidpaint(Graphicsg){
Iconicon=getIcon();
Borderb=getBorder();
g.setColor(m_bkNonSelectionColor);
g.fillRect(0,0,getWidth(),getHeight());
g.setColor(m_bkColor);
intoffset=0;
if(icon!=null&&getText()!=null){
Insetsins=getInsets();
offset=ins.left+icon.getIconWidth()+getIconTextGap();
}
g.fillRect(offset,0,getWidth()1offset,
getHeight()1);
15
if(m_hasFocus){
g.setColor(m_borderSelectionColor);
g.drawRect(offset,0,getWidth()1offset,getHeight()1);
}
setForeground(m_textColor);
setBackground(m_bkColor);
super.paint(g);
}
}
Class CarPanel
Classes ComboBox2 (formerly ComboBox1), Car, and Trim remain unchanged in this
example, so we'll start from the CarPanel class. Compared to the example in the
previous section, we've removed combo box m_cbTrims, and added JLabelm_txtModel,
which is used to display the current model's name (when the combo box popup is
hidden, the user can see only the selected trim; so we need to display the
corresponding model name separately). Curiously, the constructor of the CarPanel class
places this label component in its own JPanel (using its default FlowLayout) to ensure
it's location in the center of the base panel.
Note: The problem is that JLabelm_txtModel has a variable length, and the BoxLayout
which manages CarPanel cannot dynamically center this component correctly. By placing
this label in a FlowLayout panel it will always be centered.
The single combo box , m_cbCars, has a bit in common with the component of the same
name in the previous example. First it receives a custom model, an instance of the
CarComboBoxModel class, which will be described below. It also receives a custom
renderer, an instance of the IconComboRenderer class, also described below.
The combo box is populated by both Car and Trim instances encapsulated in ListData
objects (see below). This requires some changes in the actionPerformed() method
which handles combo box selection. First we extract the data object from the selected
ListData instance by calling the getObject() method. If this call returns a Trim object
(as it should, since Cars cannot be selected), we call the showTrim() method to display
the selected data.
Method selectCar() has been modified. As we mentioned above, our combo box now
holds ListData objects, so we cannot pass a Car object as a parameter to the
setSelectedItem() method. Instead we have to examine in turn all items in the combo
box, cast them to ListData objects, and verify that the encapsulated data object is
equal to the given Car instance. The == operator verifies that the address in memory of
the object corresponding to the combo box is the same as the address of the given
object. This assumes that the Car object passed to selectCar() is taken from the
collection of objects used to populate this combo box. (To avoid this limitation we could
alternatively implement an equals() method in the Car class.)
Method showTrim() now does the job of displaying the model data as well as the trim
data. To do this we obtain a parent Car instance for a given Trim and display the
model's name and icon. The rest of this method remains unchanged.
16
Class ListData
This class encapsulates the data object to be rendered in the combo box and adds new
attributes for our rendering needs.
Instance variables:
m_iconIcon: icon associated with the data object.
m_index int: item's index which determines the left margin (i.e. the hierarchical
level).
m_selectableboolean: flag indicating that this item can be selected.
m_dataObject: encapsulated data object.
All variables are filled with parameters passed to the constructor. The rest of the
ListData class represents four getXX() methods and a toString() method, which
delegate calls to the m_data object.
Class CarComboBoxModel
This class extends DefaultComboBoxModel to serve as a data model for our combo box .
First it creates two static ImageIcons to represent model and trim. The constructor takes
a Vector of Car instances and converts them and their trims into a linear sequence of
ListData objects. Each Car object is encapsulated in a ListData instance with an
ICON_CAR icon, index set to 0, and m_selectable flag set to false. Each Trim object is
encapsulated in a ListData instance with ICON_TRIM icon, index set to 1, and
m_selectable flag set to true.
These manipulations could have been done without implementing a custom
ComboBoxModel, of course. The real reason we do implement a custom model is to
override the setSelectedItem() method to control item selection in the combo box. As
we learned above, only ListData instances with the m_selectable flag set to true
should be selectable. To achieve this goal, the overridden setSelectedItem() method
casts the selected object to a ListData instance and examines its selection property
using isSelectable().
If isSelectable() returns false, a special action needs to be handled to move the
selection to the first item following this item for which isSelectable() returns true. If
no such item can be found our setSelectedItem() method returns and the selection in
the combo box remains unchanged. Otherwise the item variable receives a new value
which is finally passed to the setSelectedItem() implementation of the superclass
DefaultComboBoxModel.
Note: You may notice that the selectCar() method discussed above selects a Car instance
which cannot be selected. This internally triggers a call to the setSelectedItem() of the
combo box model, which shifts the selection to the first available Trim item. You can verify
this when running the example.
Class IconComboRenderer
This class extends JLabel and implements the ListCellRenderer interface to serve as a
custom combo box renderer.
Instance variables:
17
intOFFSET: offset in pixels of image and text (different for cars and trims).
Colorm_textColor: current text color.
Colorm_bkColor: current background color.
booleanm_hasFocus: flag indicating whether this item has focus.
Border[]m_borders: an array of borders used for this component.
The opaque property is set to false because we intend to draw the background
ourselves.
Method getListCellRendererComponent()is called prior to the painting of each cell in
the drop-down list. We first set this components text to that of the given object (passed
as parameter). Then, if the object is an instance of ListData, we set the icon and left
margin by using the appropriate EmptyBorder from the previously prepared array
(based on the given ListDatas m_index property--if the index is greater than the). Note
that a call to this method with row=1 will be invoked prior to the rendering of the
combo box editor, which is the part of the combo box that is always visible (see 9.1). In
this case we don't need to use any border offset. Offset only makes sense when there
are hierarchical differences between items in the list, not when an item is rendered
alone.
The rest of the getListCellRendererComponent() method determines the background
and foreground colors to use, based on whether is selected and selectable, and stores
them in instance variables for use within the paint() method. Non-selectable items
receive their own foreground to distinguish them from selectable items.
The paint() method performs a bit of rendering before invoking the super-class
implementation. It fills the background with the stored m_bkColor (from above)
excluding the icon's area (note that the left margin is already taken into account by the
component's Border). It also draws a border-like rectangle if the component currently
has the focus. This method then ends with a call to its super-classs paint() method
which takes responsibility for painting the label text and icon
Running the Code
Figure 9.2 shows our hierarchical drop-down list in action. Note that models and trim
lines can be easily differentiated because of the varying icons and offsets. In addition,
models have a gray foreground to imply that they cannot be selected.
This implementation is more user-friendly than the previous example because it
18
displays all available data in a single drop-down list. Try selecting different trims and
note how this changes data for both the model and trim information labels. Try selecting
a model and note that it will result in the selection of the first trim of that model.
UI Guideline : Improved Usability
From a usability perspective the solution in fig 9.2 is an improvement over the one
presented in fig 9.1. By using a combobox with a hierarchical data model, the designer has
reduced the data entry to a single selection and has presented the information in an
accessible and logical manner which also produces a visually cleaner result.
Further improvements could be made here by sorting the hierarchical data. In this example
it would seem appropriate to sort in a two tiered fashion: alphabetically by manufacturer;
and alphabetically by model. Thus Toyota would come after Ford and Toyota Corolla would
come after Toyota Camry.
This is an excellent example of how the programmer can improve UI Design and Usability
by doing additional work to make the User's Goal easier to achieve.
19
<<file figure9-3.gif>>
JPanelp=newJPanel();
p.setLayout(newBoxLayout(p,BoxLayout.X_AXIS));
p.add(newJLabel("Address"));
p.add(Box.createRigidArea(newDimension(10,1)));
m_locator=newMemComboBox();
m_locator.load("addresses.dat");
BrowserListenerlst=newBrowserListener();
m_locator.addActionListener(lst);
p.add(m_locator);
p.add(Box.createRigidArea(newDimension(10,1)));
20
m_runner=newAnimatedLabel("clock",8);
p.add(m_runner);
getContentPane().add(p,BorderLayout.NORTH);
m_browser=newJEditorPane();
m_browser.setEditable(false);
m_browser.addHyperlinkListener(lst);
JScrollPanesp=newJScrollPane();
sp.getViewport().add(m_browser);
getContentPane().add(sp,BorderLayout.CENTER);
WindowListenerwndCloser=newWindowAdapter(){
publicvoidwindowClosing(WindowEvente){
m_locator.save("addresses.dat");
System.exit(0);
}
};
addWindowListener(wndCloser);
setVisible(true);
m_locator.grabFocus();
}
classBrowserListenerimplementsActionListener,HyperlinkListener
{
publicvoidactionPerformed(ActionEventevt){
StringsUrl=(String)m_locator.getSelectedItem();
if(sUrl==null||sUrl.length()==0||
m_runner.getRunning())
return;
BrowserLoaderloader=newBrowserLoader(sUrl);
loader.start();
}
publicvoidhyperlinkUpdate(HyperlinkEvente){
URLurl=e.getURL();
if(url==null||m_runner.getRunning())
return;
BrowserLoaderloader=newBrowserLoader(url.toString());
loader.start();
}
}
classBrowserLoaderextendsThread
{
protectedStringm_sUrl;
publicBrowserLoader(StringsUrl){m_sUrl=sUrl;}
publicvoidrun(){
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
m_runner.setRunning(true);
try{
URLsource=newURL(m_sUrl);
m_browser.setPage(source);
m_locator.add(m_sUrl);
}
catch(Exceptione){
21
JOptionPane.showMessageDialog(Browser.this,
"Error:"+e.toString(),
"Warning",JOptionPane.WARNING_MESSAGE);
}
m_runner.setRunning(false);
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
publicstaticvoidmain(Stringargv[]){newBrowser();}
}
classMemComboBoxextendsJComboBox
{
publicstaticfinalintMAX_MEM_LEN=30;
publicMemComboBox(){
super();
setEditable(true);
}
publicvoidadd(Stringitem){
removeItem(item);
insertItemAt(item,0);
setSelectedItem(item);
if(getItemCount()>MAX_MEM_LEN)
removeItemAt(getItemCount()1);
}
publicvoidload(StringfName){
try{
if(getItemCount()>0)
removeAllItems();
Filef=newFile(fName);
if(!f.exists())
return;
FileInputStreamfStream=
newFileInputStream(f);
ObjectInputstream=
newObjectInputStream(fStream);
Objectobj=stream.readObject();
if(objinstanceofComboBoxModel)
setModel((ComboBoxModel)obj);
stream.close();
fStream.close();
}
catch(Exceptione){
e.printStackTrace();
System.err.println("Serializationerror:"+e.toString());
}
}
publicvoidsave(StringfName){
try{
FileOutputStreamfStream=
newFileOutputStream(fName);
ObjectOutputstream=
newObjectOutputStream(fStream);
stream.writeObject(getModel());
stream.flush();
stream.close();
22
fStream.close();
}
catch(Exceptione){
e.printStackTrace();
System.err.println("Serializationerror:"+e.toString());
}
}
}
classAnimatedLabelextendsJLabelimplementsRunnable
{
protectedIcon[]m_icons;
protectedintm_index=0;
protectedbooleanm_isRunning;
publicAnimatedLabel(StringgifName,intnumGifs){
m_icons=newIcon[numGifs];
for(intk=0;k<numGifs;k++)
m_icons[k]=newImageIcon(gifName+k+".gif");
setIcon(m_icons[0]);
Threadtr=newThread(this);
tr.setPriority(Thread.MAX_PRIORITY);
tr.start();
}
publicvoidsetRunning(booleanisRunning){
m_isRunning=isRunning;
}
publicbooleangetRunning(){returnm_isRunning;}
publicvoidrun(){
while(true){
if(m_isRunning){
m_index++;
if(m_index>=m_icons.length)
m_index=0;
setIcon(m_icons[m_index]);
Graphicsg=getGraphics();
m_icons[m_index].paintIcon(this,g,0,0);
}
else{
if(m_index>0){
m_index=0;
setIcon(m_icons[0]);
}
}
try{Thread.sleep(500);}catch(Exceptionex){}
}
}
}
Class Browser
This class extends JFrame to implement the frame container for our browser. Instance
variables:
JEditorPanem_browser: text component to parse and render HTML files.
23
requesting a URL.
The constructor creates the custom combo box, m_locator, and an associated
explanatory label. Then it creates the m_runner icon and places all three components in
the northern region of our frames content pane. JEditorPanem_browser is created and
placed in a JScrollPane to provide scrolling capabilities. This is then added to the
center of the content pane.
Note that the WindowListener, as used in many previous examples to close the frame
and terminate execution, receives an additional function: it invokes our custom save()
method (see below) on our custom combo box component before destroying the frame.
This saves the list of visited URLs entered as a file called addresses.dat in the current
running directory.
Class Browser.BrowserListener
This inner class implements both the ActionListener and HyperlinkListener
interfaces to manage navigation to HTML pages. The actionPerformed() method is
invoked when the user selects a new item in the combo box . It verifies that the
selection is valid and the browser is not currently running (i.e. requesting a URL). If
these checks are passed it then creates and starts a new BrowserLoader instance (see
below) for the specified address.
Method hyperlinkUpdate() is invoked when the user clicks a hyperlink in the currently
loaded web page. This method also determines the selected URL address and starts a
new BrowserLoader to load it.
Class Browser.BrowserLoader
This inner class extends Thread to load web pages into the JEditorPane component. It
takes a URL address parameter in the constructor and stores it in a instance variable.
The run() method sets the mouse cursor to hourglass ( Cursor.WAIT_CURSOR) and starts
the animated icon to indicate that the browser is busy.
The core functionality of this thread is enclosed in its try/catch block. If an exception
occurs during processing of the requested URL, it is displayed in simple dialog message
box (we will learn discuss JOptionPane in chapter 14).
The actual job of retrieving, parsing, and rendering the web page is hidden in a single
call to the setPage() method. So why do we need to create this separate thread instead
of making that simple call, say, in BrowserListener? The reason is, as we discussed in
chapter 2, by creating separate threads to do potentially time-consuming operations we
avoid clogging up the event-dispatching thread.
Class MemComboBox
This class extends JComboBox to add a historical mechanism for this component. The
constructor creates an underlying JComboBox component and sets its editable property
to true.
The add() method adds a new text string to the beginning of the list. If this item is
already present in the list, it is removed from the old position. If the resulting list is
longer than the pre-defined maximum length then the last item in the list is truncated.
24
Class AnimatedLabel
Surprisingly, Swing does not provide any special support for animated components, so
we have to create our own component for this purpose. This provides us with an
interesting example of using threads in Java.
Note: Animated GIFs are fully supported by ImageIcon (see chapter 5) but we want
complete control over each animated frame here.
26
Figure 9.4 JComboBox with custom editor suggesting previously visited URLs.
<<file figure9-4.gif>>
return;
intpos=m_editor.getCaretPosition();
Stringstr=m_editor.getText();
if(str.length()==0)
return;
for(intk=0;k<m_comboBox.getItemCount();k++){
Stringitem=m_comboBox.getItemAt(k).toString();
if(item.startsWith(str)){
m_editor.setText(item);
m_editor.setCaretPosition(item.length());
m_editor.moveCaretPosition(pos);
break;
}
}
}
}
Class Browser
This class has only one change in comparison with the previous example: it creates an
instance of our custom MemComboAgent class and passes it a reference to our
m_locator combo box.
Class MemComboAgent
This class extends KeyAdapter to listen for keyboard activity. It takes a reference to a
JComboBox component and stores it in an instance variable along with the JTextField
component used as that combo boxs editor. Finally, a MemComboAgent object adds itself
to that editor as a KeyListener to be notified of all keyboard input that is passed to the
editor component.
Method keyReleased() is the only method we implement. First this method retrieves
the pressed characters and verifies that they are not control characters. We also
retrieve the contents of the text field and check that it is not empty (to avoid annoying
the user with suggestions in an empty field). Note that when this method is invoked the
pressed key will already have been included in this text.
This method then walks through the list of combo box items and searches for an item
starting with the combo box editor text. If such an item is found it is set as the combo
box editors text. Then we place the caret at the end of that string using
setCaretPosition(), and move it back to its initial position in the backward direction
using the moveCaretPosition() method. This final JTextComponent method places the
caret in its original position and highlights all text to its right (see chapters 11 and 19).
Note: A more sophisticated realization of this idea may include separate processing of URL
protocol and host, as well as using threads for smooth execution.
29