javablogspot

Just another WordPress.com weblog

The Swing Cookbook

Posted by damuchinni on March 11, 2009

an Darwin’s Java Cookbook presents a collection of recipes for solving hundreds of problems that Java developers encounter. For example, in the first edition, one of Chapter 2’s recipes discusses how to use the classpath effectively. To the best of my knowledge, O’Reilly hasn’t published a similar cookbook that deals exclusively with Swing.

A Swing-only cookbook might make Swing more accessible to those inexperienced with this toolkit. Perhaps it would help for Chapter 1’s recipes to target the Swing Application Framework, which is intended to make Swing applications easier to write, and which should debut in Java SE 7. Later chapters could each target one or more Swing components.

For example, one chapter might present recipes for both the JList and JComboBox components. (Because of their similarity, grouping JList and JComboBox recipes in the same chapter seems logical.) Although some recipes would be JList-specific, and other recipes would target only JComboBox, still other recipes would focus on both components. Custom cell rendering is one example.

JList and JComboBox each manage a list of items. They rely on a cell renderer component to render each item in a cell. Although the default cell renderer is adequate for most rendering tasks, scenarios exist where you’ll want to install a custom cell renderer to handle a specific kind of rendering. For example, Figure 1 shows a custom cell renderer presenting a flag icon beside a country’s name.

Figure 1: The flag icons are courtesy of British developer Mark James.

Prior to rendering an item in a cell, JList and JComboBox obtain an item-specific rendering component by invoking the ListCellRenderer interface’s Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) method. The returned component performs the rendering, taking into account the method’s arguments:

list references the JList whose cell is to be rendered. (For a JComboBox, list references the JList part of this component.)
value references the object that is to be rendered in the cell.
index identifies value’s location in the list. In some cases, such as when focus shifts away from the list, -1 is passed to index.
isSelected indicates whether the cell being rendered is currently selected (true) or not (false).
cellHasFocus indicates whether the cell being rendered has the input focus (true) or not (false). For JComboBox, this argument is always false because a combobox’s text field, instead of the list, gets the focus.
JList and JComboBox default their cell renderers to instances of the DefaultListCellRenderer class, which implements ListCellRenderer. These defaults can be replaced by invoking JList’s void setCellRenderer(ListCellRenderer cellRenderer) and JComboBox’s void setRenderer(ListCellRenderer aRenderer) methods.

Before invoking setCellRenderer() or setRenderer(), you need to declare and instantiate a class that implements ListCellRenderer and provides the rendering logic. The easiest way to handle the rendering in a look-and-feel independent manner is to subclass an existing component such as a JLabel. This is precisely what the custom cell renderer illustrated in Figure 1 and specified in Listing 1 accomplishes.

Listing 1: Countries.java

// Countries.java

import java.awt.*;

import java.net.URL;

import java.util.*;

import javax.swing.*;
import javax.swing.border.Border;

public class Countries extends JFrame
{
Countries (String title)
{
super (title);
setDefaultCloseOperation (EXIT_ON_CLOSE);

FlowLayout layout = new FlowLayout (FlowLayout.CENTER, 5, 5);
layout.setAlignOnBaseline (true);
getContentPane ().setLayout (layout);

Country [] carray;
JList list = new JList (carray = createCountriesArray ());
// list.setEnabled (false);
list.setCellRenderer (new CountryCellRenderer (null));
list.setVisibleRowCount (8);
list.setSelectedIndex (0);
getContentPane ().add (new JScrollPane (list));

JComboBox cb = new JComboBox (carray);
// cb.setEnabled (false);
cb.setRenderer (new CountryCellRenderer (cb));
getContentPane ().add (cb);

pack ();
setVisible (true);
}

Country [] createCountriesArray ()
{
String [] citems =
{
“AD,Andorra”,
“AE,United Arab Emirates”,
“AF,Afghanistan”,
“AG,Antigua and Barbuda”,
“AI,Anguilla”,
“AL,Albania”,
“AM,Armenia”,
“AN,Netherlands Antilles”,
“AO,Angola”,
“AR,Argentina”,
“AS,American Samoa”,
“AT,Austria”,
“AU,Australia”,
“AW,Aruba”,
“AX,Åland Islands”,
“AZ,Azerbaijan”,
“BA,Bosnia and Herzegovina”,
“BB,Barbados”,
“BD,Bangladesh”,
“BE,Belgium”,
“BF,Burkina Faso”,
“BG,Bulgaria”,
“BH,Bahrain”,
“BI,Burundi”,
“BJ,Benin”,
“BM,Bermuda”,
“BN,Brunei Darussalam”,
“BO,Bolivia”,
“BR,Brazil”,
“BS,Bahamas”,
“BT,Bhutan”,
“BV,Bouvet Island”,
“BW,Botswana”,
“BY,Belarus”,
“BZ,Belize”,
“CA,Canada”,
“CC,Cocos (Keeling) Islands”,
“CD,Congo, the Democratic Republic of the”,
“CF,Central African Republic”,
“CG,Congo”,
“CH,Switzerland”,
“CI,Cote d’Ivoire Côte d’Ivoire”,
“CK,Cook Islands”,
“CL,Chile”,
“CM,Cameroon”,
“CN,China”,
“CO,Colombia”,
“CR,Costa Rica”,
“CU,Cuba”,
“CV,Cape Verde”,
“CX,Christmas Island”,
“CY,Cyprus”,
“CZ,Czech Republic”,
“DE,Germany”,
“DJ,Djibouti”,
“DK,Denmark”,
“DM,Dominica”,
“DO,Dominican Republic”,
“DZ,Algeria”,
“EC,Ecuador”,
“EE,Estonia”,
“EG,Egypt”,
“EH,Western Sahara”,
“ER,Eritrea”,
“ES,Spain”,
“ET,Ethiopia”,
“FI,Finland”,
“FJ,Fiji”,
“FK,Falkland Islands (Malvinas)”,
“FM,Micronesia, Federated States of”,
“FO,Faroe Islands”,
“FR,France”,
“GA,Gabon”,
“GB,United Kingdom”,
“GD,Grenada”,
“GE,Georgia”,
“GF,French Guiana”,
“GH,Ghana”,
“GI,Gibraltar”,
“GL,Greenland”,
“GM,Gambia”,
“GN,Guinea”,
“GP,Guadeloupe”,
“GQ,Equatorial Guinea”,
“GR,Greece”,
“GS,South Georgia and the South Sandwich Islands”,
“GT,Guatemala”,
“GU,Guam”,
“GW,Guinea-Bissau”,
“GY,Guyana”,
“HK,Hong Kong”,
“HM,Heard Island and McDonald Islands”,
“HN,Honduras”,
“HR,Croatia”,
“HT,Haiti”,
“HU,Hungary”,
“ID,Indonesia”,
“IE,Ireland”,
“IL,Israel”,
“IN,India”,
“IO,British Indian Ocean Territory”,
“IQ,Iraq”,
“IR,Iran, Islamic Republic of”,
“IS,Iceland”,
“IT,Italy”,
“JM,Jamaica”,
“JO,Jordan”,
“JP,Japan”,
“KE,Kenya”,
“KG,Kyrgyzstan”,
“KH,Cambodia”,
“KI,Kiribati”,
“KM,Comoros”,
“KN,Saint Kitts and Nevis”,
“KP,Korea, Democratic People’s Republic of”,
“KR,Korea, Republic of”,
“KW,Kuwait”,
“KY,Cayman Islands”,
“KZ,Kazakhstan”,
“LA,Lao People’s Democratic Republic”,
“LB,Lebanon”,
“LC,Saint Lucia”,
“LI,Liechtenstein”,
“LK,Sri Lanka”,
“LR,Liberia”,
“LS,Lesotho”,
“LT,Lithuania”,
“LU,Luxembourg”,
“LV,Latvia”,
“LY,Libyan Arab Jamahiriya”,
“MA,Morocco”,
“MC,Monaco”,
“MD,Moldova, Republic of”,
“ME,Montenegro”,
“MG,Madagascar”,
“MH,Marshall Islands”,
“MK,Macedonia, the former Yugoslav Republic of”,
“ML,Mali”,
“MM,Myanmar”,
“MN,Mongolia”,
“MO,Macao”,
“MP,Northern Mariana Islands”,
“MQ,Martinique”,
“MR,Mauritania”,
“MS,Montserrat”,
“MT,Malta”,
“MU,Mauritius”,
“MV,Maldives”,
“MW,Malawi”,
“MX,Mexico”,
“MY,Malaysia”,
“MZ,Mozambique”,
“NA,Namibia”,
“NC,New Caledonia”,
“NE,Niger”,
“NF,Norfolk Island”,
“NG,Nigeria”,
“NI,Nicaragua”,
“NL,Netherlands”,
“NO,Norway”,
“NP,Nepal”,
“NR,Nauru”,
“NU,Niue”,
“NZ,New Zealand”,
“OM,Oman”,
“PA,Panama”,
“PE,Peru”,
“PF,French Polynesia”,
“PG,Papua New Guinea”,
“PH,Philippines”,
“PK,Pakistan”,
“PL,Poland”,
“PM,Saint Pierre and Miquelon”,
“PN,Pitcairn”,
“PR,Puerto Rico”,
“PS,Palestinian Territory, Occupied”,
“PT,Portugal”,
“PW,Palau”,
“PY,Paraguay”,
“QA,Qatar”,
“RE,Reunion Réunion”,
“RO,Romania”,
“RS,Serbia”,
“RU,Russian Federation”,
“RW,Rwanda”,
“SA,Saudi Arabia”,
“SB,Solomon Islands”,
“SC,Seychelles”,
“SD,Sudan”,
“SE,Sweden”,
“SG,Singapore”,
“SH,Saint Helena”,
“SI,Slovenia”,
“SJ,Svalbard and Jan Mayen”,
“SK,Slovakia”,
“SL,Sierra Leone”,
“SM,San Marino”,
“SN,Senegal”,
“SO,Somalia”,
“SR,Suriname”,
“ST,Sao Tome and Principe”,
“SV,El Salvador”,
“SY,Syrian Arab Republic”,
“SZ,Swaziland”,
“TC,Turks and Caicos Islands”,
“TD,Chad”,
“TF,French Southern Territories”,
“TG,Togo”,
“TH,Thailand”,
“TJ,Tajikistan”,
“TK,Tokelau”,
“TL,Timor-Leste”,
“TM,Turkmenistan”,
“TN,Tunisia”,
“TO,Tonga”,
“TR,Turkey”,
“TT,Trinidad and Tobago”,
“TV,Tuvalu”,
“TW,Taiwan, Province of China”,
“TZ,Tanzania, United Republic of”,
“UA,Ukraine”,
“UG,Uganda”,
“UM,United States Minor Outlying Islands”,
“US,United States”,
“UY,Uruguay”,
“UZ,Uzbekistan”,
“VA,Holy See (Vatican City State)”,
“VC,Saint Vincent and the Grenadines”,
“VE,Venezuela”,
“VG,Virgin Islands, British”,
“VI,Virgin Islands, U.S.”,
“VN,Viet Nam”,
“VU,Vanuatu”,
“WF,Wallis and Futuna”,
“WS,Samoa”,
“YE,Yemen”,
“YT,Mayotte”,
“ZA,South Africa”,
“ZM,Zambia”,
“ZW,Zimbabwe”
};

ArrayList clist = new ArrayList ();
for (String citem: citems)
{
String [] cdata = citem.split (“,”);
clist.add (new Country (cdata [1],
getClass ().getResource (“icons/”+
cdata [0].toLowerCase ()+
“.png”)));
}

Country [] carray = clist.toArray (new Country [0]);
Arrays.sort (carray);
return carray;
}

public static void main (String [] args)
{
Runnable r = new Runnable ()
{
public void run ()
{
// Always create Swing UIs on event-dispatching
// thread.

new Countries (“Countries”);
}
};
EventQueue.invokeLater (r);
}
}

class Country implements Comparable
{
private String name;
private ImageIcon flagIcon;

private URL path;

Country (String name, URL path)
{
this.name = name;
this.path = path;
}

String getName ()
{
return name;
}

ImageIcon getFlagIcon ()
{
// Lazily load flag icon. Make sure that each country’s flag icon is
// loaded only once.

if (flagIcon == null)
flagIcon = new ImageIcon (path);

return flagIcon;
}

public int compareTo (Country o)
{
return name.compareTo (o.name);
}
}

class CountryCellRenderer extends JLabel implements ListCellRenderer
{
private Border border;

private JComboBox cb;

CountryCellRenderer (JComboBox cb)
{
this.cb = cb;

// Leave a 10-pixel separator between the flag icon and country name.

setIconTextGap (10);

// Swing labels default to being transparent; the container’s color
// shows through. To change a Swing label’s background color, you must
// first make the label opaque (by passing true to setOpaque()). Later,
// you invoke setBackground(), passing the new color as the argument.

setOpaque (true);

// This border is placed around a cell that is selected and has focus.

border = BorderFactory.createLineBorder (Color.RED, 1);
}

public Component getListCellRendererComponent (JList list,
Object value,
int index,
boolean isSelected,
boolean cellHasFocus)
{
Country c = (Country) value;
setText (c.getName ());
setIcon (c.getFlagIcon ());

if (isSelected)

{
setBackground (list.getSelectionBackground ());
setForeground (list.getSelectionForeground ());
}
else
{
setBackground (list.getBackground ());
setForeground (list.getForeground ());
}

setFont (list.getFont ());

// list.isEnabled() always returns true for a combobox’s list, even if
// the combobox is disabled.

setEnabled (cb != null ? cb.isEnabled () : list.isEnabled ());

if (isSelected && cellHasFocus) // cellHasFocus ignored for JComboBox
setBorder (border);
else
setBorder (null);

return this;
}
}

Listing 1 is organized into Countries, Country, and CountryCellRenderer classes. The main Countries class creates the user interface and stores the URLs of various flag icons along with country names into an array of Country objects. Each of these objects is passed to CountryCellRenderer’s getListCellRendererComponent() method to obtain a JLabel that can render the object.

These classes offer a variety of techniques that are worth remembering for your own Swing applications and custom cell renderers:

JFrame’s content pane defaults to a JPanel managed by a BorderLayout layout manager. Rather than wrap a component in a JPanel and add that panel to the content pane’s panel, to display the component at its preferred size, simply set the content pane’s panel’s layout manager to a FlowLayout, which makes every effort to display components at their preferred sizes. Wrapping a component in a JPanel, which is then added to the content pane’s panel, also works (but results in unnecessary JPanel creation) because JPanel’s default layout manager is FlowLayout.
Java SE 6 introduced the void setAlignOnBaseline(boolean alignOnBaseline) method to FlowLayout to allow components to be vertically aligned according to their baselines (when true is passed to alignOnBaseline). This method makes it possible to present the combobox near the top of its container instead of in the middle.
An instance of a custom cell renderer class can be shared among multiple JLists and JComboBoxs provided that the class provides no instance-specific fields. For example, Listing 1’s CountryCellRenderer class contains a cb field. This field is set to null if the custom cell renderer is assigned to a JList, or the JComboBox instance on which the custom cell renderer is being assigned. The JComboBox is needed to determine if this component is disabled, whereas the list reference passed to getListCellRendererComponent() is sufficient for determining whether a JList is disabled. Because the cb field has a dual role, Listing 1 requires two instances of CountryCellRenderer.
The Country class’s getFlagIcon() method uses ImageIcon to load the flag icon. It lazily loads this icon when actually required, and also to ensure that the flag icon is loaded only once — the custom cell renderer invokes getFlagIcon() multiple times for each country.
The Country class implements Comparable to allow the array of Country objects (created in the Countries class’s createCountriesArray() method) to be sorted (via Arrays.sort()) according to country name.
DefaultListCellRenderer supports setting a border for a focused JList’s selected item. CountryCellRenderer’s getListCellRendererComponent() method also sets a red border for the selected item of a focused JList, to show that this component is focused. Rather than create a border object each time getListCellRendererComponent() is invoked, which is inefficient, I create the border exactly once, in CountryCellRenderer’s constructor.
Although it’s convenient for CountryCellRenderer to offload the rendering task to JLabel, which handles icon and text rendering, there is a problem with JLabel that must be overcome before it can be used to show a selected item. Specifically, JLabel’s opacity defaults to transparent, which allows the underlying container’s background color to show through. As a result, a selection bar never appears. You can fix this problem by making JLabel opaque, which is accomplished in CountryCellRenderer’s constructor.
Finally, it’s important to create as few objects as possible in getListCellRendererComponent() because this method is frequently called. Creating too many objects on the heap can lead to garbage collections pauses that can disrupt the smooth flow of program execution. Fortunately, getListCellRendererComponent() creates no objects (unless I’m mistaken). Furthermore, it reuses the same JLabel object.
After compiling Countries.java, it’s easy to package this application into an executable JAR file by first creating a manifest file that contains Main-Class: Countries and (assuming the manifest file is named manifest.mf) executing jar cfm Countries.jar manifest.mf *.class icons. You then specify java -jar Countries.jar to execute this application.

With a bit of work, the previous cell-rendering material could be turned into a recipe in the hypothetical Swing cookbook’s chapter on JList and JComboBox. (Recipes on rendering table and tree cells could appear in chapters that deal with JTable and JTree.) However, is creating a Swing cookbook a good idea, especially in light of JavaFX, which simplifies user interface development?

Leave a comment