Java AWT and JFC idioms

JList

String[] data = {"one", "two", "three"}; JList list = new JList(data); JScrollPane scroll = new JScrollPane(list); list.setSelectedIndex(0); // selects "one" list.getSelectedIndex(); // returns 0 list.getSelectedValue(); // returns "one"

Cell Rendering

JList, JTable and JTree ("receivers") all employ CellRenderer components to paint their cells. Certain default components are available, but you can define your own. Each time the receiver needs to paint a (row, cell, node) it asks the renderer for a component (a subclass of java.awt.Component) configured in a certain way, emplaces it with setBounds(), and "stamps" it in place with paint().

Remember, what "implementing an interface" means is that a concrete class provides code for all the methods declared in the interface. Since a renderer subclasses some component (sensibly a Swing component, most frequently JLabel), the only new code you need to write is an implementation for the single getXXXCellRendererComponent() method of the XXXCellRenderer interface.

In the example below, a JList displays TrekData objects (defined elsewhere), using a custom ListCellRenderer that extends JLabel. When the JList (automatically, repeatedly) calls getListCellRendererComponent() on the (anonymous, internal) renderer, it passes five arguments: a reference to itself, the TrekData object being rendered, the index of the row, whether the row is selected, and whether the row has mouse focus. The renderer (JLabel subclass) can use any or all of these arguments to configure itself. In this case, it retrieves a symbolic int value from the passed-in TrekData that defines the nationality of a starship, and uses it to set the JLabel's text and icon. It also reads the isSelected argument to see if the "reverse video" selection effect needs to be painted, via setBackground().

The TableCellRenderer and TreeCellRenderer operate in the same way, except that their receivers know how to paint table cells and collapsible tree nodes, respectively.

JList trekList = new JList(); trekList.setCellRenderer(new TrekRenderer()); class TrekRenderer extends com.sun.java.swing.JLabel implements com.sun.java.swing.ListCellRenderer { final static ImageIcon icoSF = new ImageIcon("starfleet.gif"); final static ImageIcon icoKDF = new ImageIcon("klingon.gif"); final static ImageIcon icoRSE = new ImageIcon("romulan.gif"); final static ImageIcon icoUnk = new ImageIcon("unknown.gif"); public Component getListCellRendererComponent( JList list, // the receiver Object value, // a particular entry within the receiver int index, // the entry's physical index within the receiver boolean isSelected, // is the index selected? boolean hasFocus) // does the index have focus? { int nation = value.getNation(); // get starship nationality key setText(TrekData.getNationString(nation)); // get nationality string switch(nation){ // decide on an icon case(TrekData.NAT_SF): setIcon(icoSF); break; case(TrekData.NAT_KDF): setIcon(icoKDF); break; case(TrekData.NAT_RSE): setIcon(icoRSE); break; default: setIcon(icoUnk); } // Other code here for selection and focus cosmetics return this; } }

If you want to select rows with a "reverse video" effect, you first have to setOpaque(true) the renderer component. Swing components are transparent by default (i.e., the background color is transparent), so if you don't, the color of the underlying receiver will show right through. You can obtain the selection and unselected colors from the receiver, and remember to revert the colors if the isSelected argument is false.

this.setOpaque(true); if(isSelected){ super.setBackground(list.getSelectionBackground()); super.setForeground(list.getSelectionForeground()); }else{ super.setBackground(list.getBackground()); super.setForeground(list.getForeground()); }

If your receiver has keyboard focus and is editable, your renderer should paint itself in a distinctive fashion. The following example retrieves the focus properties from the static UIManager object; if not in focus, it applies an EmptyBorder (2 pixels at the sides, 1 pixel top and bottom) to the renderer. Note that, if the border widths don't line up exactly, this can cause the JLabel to shift by a small amount.

if(hasFocus){ this.setBorder( UIManager.getBorder("List.focusCellHighlightBorder") ); if( /* row is editable */ ){ super.setForeground( UIManager.getColor("List.focusCellForeground") ); super.setBackground( UIManager.getColor("List.focusCellBackground") ); } }else{ this.setBorder(new com.sun.java.swing.border.EmptyBorder(1, 2, 1, 2)); }

Scrolling

The only Swing component with scrolling behavior is JScrollPane. [?What about JComboBox?] Others that typically scroll (JTable, JTree, JList) must become a client of a JViewport embedded within a JScrollPane. Fortunately, there are JScrollPane convenience constructors that take one of these three components.

JList list = new JList(data); JTable tbl = new JTable(new AbstractTableModel()); JScrollPane scroll = new JScrollPane(list); JScrollPane scroll = new JScrollPane(tbl);

Mouse Events

Many apps rely on a combination of which mouse button was pressed, whether it was (single, double, triple)-clicked, and whether any modifier keys (shift, control, alt, meta) were held on the keyboard at the same time.

The following example creates an anonymous [? really ?] subclass of java.awt.event.MouseAdapter (which can be declared as type java.awt.event.MouseListener because MouseAdapter implements that interface). It overrides/implements only one of the five methods, mouseClicked(). It can refer to list because the latter is declared final.

final JList list = new JList(mdl); MouseListener clickListener = new MouseAdapter(){ public void mouseClicked(MouseEvent e){ int index = list.locationToIndex(e.getPoint()); Object value = list.getModel().getElementAt(index); switch(e.getClickCount()){ case 1: System.out.println("Single-clicked on item " + value.toString()); break; case 2: System.out.println("Double-clicked on item #" + index); break; case 3: System.out.println("Triple-clicked on item #" + index); break; } } }; list.addMouseListener(clickListener);

Or we could write it this way, passing an anonymous Adapter class to addMouseListener():

JList list = new JList(mdl); list.addMouseListener( new MouseAdapter(){ public void mouseClicked(MouseEvent e){ ... } });

Or this way, with a named inner (or outer) class:

JList list = new JList(mdl); list.addMouseListener(new ClickListener(list)); public class ClickListener extends MouseAdapter{ JList list; ClickListener(JList l){ super(); this.list = l; } public void mouseClicked(MouseEvent e){ ... } }

CheckBoxes and RadioButtons

CheckBoxListener cboxLis = new CheckBoxListener(); for(int i=0; i<buttonDatums.length; i++){ buttons[i] = new JCheckBox(buttonDatums[i].label); // String buttons[i].setMnemonic(buttonDatums[i].mnemonic); // char buttons[i].setSelected(buttonDatums[i].selected); // boolean buttons[i].addItemListener(cboxLis); // ItemListener } class CheckBoxListener implements ItemListener{ public void itemStateChanged(ItemEvent e){ Object src = e.getItemSelectable(); // synonym for getSource() for(int i=0; i<buttons.length; i++){ if(src == buttons[i]){ process(buttonDatums[i].data); } if(e.getStateChange() == ItemEvent.DESELECTED){ // or ItemEvent.SELECTED buttons[i].setIcon(DESEL_ICON); } } } }
public static final int ACT_INVERT = 100; public static final int ACT_TRANS_X = 101; public static final int ACT_TRANS_Y = 102; int[] buttonCmds = new int[]{ACT_INVERT, ACT_TRANS_X, ACT_TRANS_Y}; RadioListener radioLis = new RadioListener(); for(int i=0; i<buttonDatums.length; i++){ buttons = new JRadioBox(buttonDatums[i].label); // String buttons[i].setActionCommand(Integer.toString(buttonCmds[i])); buttons[i].addActionListener(radioLis); // ActionListener } class RadioListener implements java.awt.event.ActionListener{ public void actionPerformed(java.awt.event.ActionEvent e){ String str = e.getActionCommand(); int cmd = Integer.parseInt(str); switch(cmd){ case ACT_INVERT: ... case ACT_TRANS_X: ... case ACT_TRANS_Y: ... default: ... } } }

Box and BoxLayout

The com.sun.java.swing.BoxLayout layout manager organizes the contents of its clients in a single horizontal or vertical row, along with optional spacers. A series of boxed JPanels can do everything java.awt.GridLayout can (with better control), and can emulate a java.awt.GridBagLayout with much greater simplicity. The com.sun.java.swing.Box lightweight container is limited to using BoxLayout, and may be convenient if you don't want to use a Border on it.

The spacers created by the Box.createHorizontalGlue() and createVerticalGlue() are misnamed, IMHO. You think of glue as pulling objects together. Such an object acts more like a spring (but could be confused with the now-defunct SpringLayout), sponge, wadding or gas: it expands to fill the available space between other components, pressing them against the edges of the container.

The inner class Box.Filler is more constrained. Its constructor takes minimum, preferred and maximum sizes (java.awt.Dimension objects, typically based on the size of the components around it) and stretches to ensure at-least-this-much but not-more-than-this space is inserted.

JPanel pnl = new JPanel(); pnl.setLayout(new BoxLayout(pnl, BoxLayout.X_AXIS)); // horizontal layout pnl.add(new JLabel("One")); pnl.add(Box.createHorizontalGlue()); // separate the two JLabels JLabel lbl = new JLabel("Two")); pnl.add(lbl) Dimension minDim = new Dimension(5,10); // at least 5 pix horiz sep Dimension prefDim = lbl.getPreferredSize(); Dimension maxDim = lbl.getMaximumSize(); pnl.add(new Box.Filler(minDim, prefDim, maxDim)) pnl.add(new JLabel("Three"));

Resource Bundles

PropertyResourceBundles (which are merely text files with the extension .properties) are capable of storing only strings. However, with the right parser, it's easy enough to translate those strings into other objects. The example below retrieves a string, a string that sets the text on a JButton, a string that's the URL for an Icon for the JButton, and the subclass for a JMenuItem. The string keys in the props file can be anything you want, but typically they're formatted as dot-delimited hierarchies. Space within a value is significant, but outside is not. You can continue onto subsequent lines with a backslash character. Comments are indicated by a hash (#) in the first column (as with shell scripts and Perl), and lines can be continued with a backslash.

## Props file guiMain.properties

about.copyright  =(c)2001 Underbase Corp.
about.dept       =Cybertron Third Age
about.email      =archivist@c3a.underbase.org

dialog.button.txt=Revert
dialog.button.ico=Icons/revert.gif
dialog.button.tip=\
Press this button to return the database to its last saved state.

menubar.edit.delete.class = JMenu
menubar.edit.format.class = JCheckBoxMenuItem
menubar.view.normal.class = JRadioButtonMenuItem
menubar.view.outline.class= JRadioButtonMenuItem
menubar.view.preview.class= JRadioButtonMenuItem

Load the bundle, then some properties from it. Note that the .properties extension is omitted from the getBundle() method.

ResourceBundle res = ResourceBundle.getBundle("guiMain"); String strCopyr = res.getString("about.copyright"); JButton btn = new JButton(); btn.setText(res.getString("dialog.button.txt")); btn.setIcon(new Icon(res.getString("dialog.button.ico"))); Class menuItemClass = Class.forName(res.getString("menubar.edit.delete.class")); JMenuItem item = menuItemClass.newInstance();

An equivalent ListResourceBundle subclass is as follows. The only method that need be overridden is getContents, which should return an Object[][] array, with the first column being the String keys. Note that, unlike in a non-code PropertiesResourceBundle, a ListResourceBundle can actually instantiate and maintain references to objects, so some of the above properties are no longer necessary. On the flip side, you have to recompile this to make changes, whereas you can simply text-edit a PropertiesResourceBundle and restart an applet.

import java.util.ListResourceBundle; import java.awt.Button; public class GUIProps extends java.util.ListResourceBundle { public Object getContents() { return _contents; } static Object[][] _contents = { {"about.copyright", "(c)2001 Underbase Corp." }, {"about.dept", "Cybertronian Third Age" }, {"about.email", "archivist@c3a.underbase.org"}, {"dialog.button", new Button("Revert", new ImageIcon("Icons/revert.gif")}, {"dialog.button.tip", "Press this button to return the " + "database to its last saved state." }, {"menubar.edit.delete", new JMenu() }, {"menubar.edit.format", new JCheckBoxMenuItem() }, {"menubar.view.normal", new JRadioButtonMenuItem() } } }