The Swing toolkit offers a variety of tools for creating user interfaces and an almost bewildering array of options for modifying those interfaces during a program's lifetime. Careful use of these features can result in interfaces that dynamically adapt to the user's needs and simplify interaction. Careless use of the same features can lead to very confusing or even completely unusable programs. This article introduces the technology and philosophy of dynamic UIs and gives you a leg up on building effective ones. You'll modify source code that's based on the SwingSet2 demo application provided with the Sun JDK (see Resources); this application's UI uses a number of dynamic features and serves as an excellent starting point for understanding them.
The simplest form of dynamic UI is one that grays out menu items or buttons that are unavailable. Disabling UI widgets works the same way for all widgets; the setEnabled() function is a feature of the Component class. Listing 1 shows the code for disabling a button:
Listing 1. Disabling a button
button.setEnabled(false); |
Easy enough, as you can see. The question is when you should enable or disable a button. One common design decision is to disable a button when it's not applicable. For instance, many programs disable the Save button (and any corresponding menu item) when a file hasn't been modified since the last time it was saved.
The major caveat with disabling buttons is to remember to re-enable them at an appropriate time. For example, if there's a confirmation step between clicking on a button and the completion of its action, the button should be re-enabled even if confirmation fails.
Sometimes an application needs to adjust the range of a numeric widget, such as a Spinner or Slider, dynamically. This can be a lot more complicated than it looks. Sliders, in particular, have secondary features -- ticks, tick spacing, and labels -- that might need to be adjusted along with the range to avoid catastrophic ugliness.
The SwingSet2 demo doesn't directly do any of this, so you'll modify it by attaching a ChangeListener to one slider that can modify another. Enter the new SliderChangeListener class, shown in Listing 2:
Listing 2. Changing a slider's range
class SliderChangeListener implements ChangeListener {
JSlider h;
SliderChangeListener(JSlider h) {
this.h = h;
}
public void stateChanged(ChangeEvent e) {
JSlider js = (JSlider) e.getSource();
int i = js.getValue();
h.setMaximum(i);
h.repaint();
}
}
|
When the third horizontal slider is created (the one that in the original demo has tick marks every unit and labels at 5, 10, and 11), a new SliderChangeListener is also created, passing the slider as the constructor argument. When the third vertical slider (with a range of 0 to 100) is created, the new SliderChangeListener is added to it as a change listener. This works about as expected: Adjusting the vertical slider changes the range of the horizontal slider.
Unfortunately, the ticks and labels don't work well at all. The labels every five ticks work out okay as long as the range doesn't get too large, but the extra label at 11 quickly becomes a usability problem, as shown in Figure 1:
Figure 1. Labels running together
The obvious solution would be simply to set the tick spacing on the horizontal slider whenever its maximum value is updated, as shown in Listing 3:
Listing 3. Setting tick spacing
// DOES NOT WORK int tickMajor, tickMinor; tickMajor = (i > 5) ? (i / 5) : 1; tickMinor = (tickMajor > 2) ? (tickMajor / 2) : tickMajor; h.setMajorTickSpacing(tickMajor); h.setMinorTickSpacing(tickMinor); h.repaint(); |
Listing 3 is correct as far as it goes, but it doesn't result in any change to the labels drawn on screen. You must set the labels separately, using setLabelTable(). Adding one more line fixes it:
h.setLabelTable(h.createStandardLabels(tickMajor)); |
This still leaves you with the odd label at 11 that was set up originally. The intent, of course, is to have a label always at the right end of the slider. You can do this by removing the old label (before setting the new maximum value) and then adding a new one. This code almost works:
Listing 4. Replacing labels
public void stateChanged(ChangeEvent e) {
JSlider js = (JSlider) e.getSource();
int i = js.getValue();
// clear old label for top value
h.getLabelTable().remove(h.getMaximum());
h.setMaximum(i);
int tickMajor, tickMinor;
tickMajor = (i > 5) ? (i / 5) : 1;
tickMinor = (tickMajor > 2) ? (tickMajor / 2) : tickMajor;
h.setMajorTickSpacing(tickMajor);
h.setMinorTickSpacing(tickMinor);
h.setLabelTable(h.createStandardLabels(tickMajor));
h.getLabelTable().put(new Integer(i),
new JLabel(new Integer(i).toString(), JLabel.CENTER));
h.repaint();
}
|
If I've told you once, I've told you twice
By almost I mean that, although the code in Listing 4 removes the label at 11, it doesn't add the new label at i; instead, you see only the labels at tickMajor intervals. The solution is rather shocking at first:
Listing 5. Forcing a display update
h.setLabelTable(h.getLabelTable()); |
This seemingly pointless operation actually has a significant effect. The labels for a slider are generated whenever the label table is set. There's no special callback for modifications to the table, so new values added to the table don't necessarily have an effect; the apparent no-op in Listing 5 has the side effect of letting Swing know it must update the display. (Lest you think I invented this myself, notice that the original SwingSet code includes such a call.)
This leaves only one problem. The very reasonable desire to ensure that a label appears at the end of the slider sometimes puts two labels immediately adjacent to each other, or even overlapping, as shown in Figure 2:
Figure 2. Overlapping labels at the end of the slider
A number of solutions to this problem are possible. One is to write your own code to populate the label table with values and stop the sequence earlier so that the last label in the sequence is somewhat separated from the end of the slider. I'll leave this one as an exercise for you.
In many cases, it's quite practical to restrict menu modifications to enabling and disabling menu items. This approach is subject to the general caveat that applies to disabling items: Avoid leaving your program in an unusable state accidentally by disabling crucial items.
It's also possible to add or remove menu items or submenus. It's not as easy to modify a JMenuBar; there's no interface for removing or replacing individual menus from the bar. If you want to modify a bar (other than by adding new menus to its right end), you need to make a new bar and replace the old one with it.
Modifications to individual menus take effect immediately; you don't need to build a menu before attaching it to a bar or another menu. When you need to modify your selection of menu options, the easiest way is to modify a given menu. Still, you might want to add and remove entire menus, and it's not particularly hard to do so. Listing 6 shows a simple example of a method to insert a menu into a menu bar before a given index. This example assumes that the JMenuBar to be replaced is attached to a JFrame object, but anything that lets you get and set menu bars will work the same way:
Listing 6. Inserting a menu into a menu bar
public void insertMenu(JFrame frame, JMenu menu, int index) {
JMenuBar newBar = new JMenuBar();
JMenuBar oldBar = frame.getJMenuBar();
MenuElement[] oldMenus = oldBar.getSubElements();
int count = oldBar.getMenuCount();
int i;
for (i = 0; i < count; ++i) {
if (i == index)
newBar.add(menu);
newBar.add((JMenu) oldMenus[i]);
}
frame.setJMenuBar(newBar);
}
|
This code is not what I first attempted; this final version, nicely fixed up so that it works, reflects a handful of interesting quirks. It might seem at first that the obvious way to implement this would be to use getComponentAtIndex(), but that's been deprecated. Luckily, the getSubElements() interface is good enough. The cast to JMenu for newBar.add() is probably safe, but I don't like it. The getSubElements() interface works on menus, not just menu bars; menus can have subelements of several types, but JMenus are the only elements you can add to a JMenuBar. So you must cast the element to a JMenu to pass it to the JMenuBar.add() method. Unfortunately, if a future API revision lets you add elements of types other than JMenu to a JMenuBar, it will no longer be necessary, or even safe, to cast the returned elements to JMenu.
The code in Listing 6 reflects one other rather subtle interface quirk; the menu count must be cached in advance. When the menus are added to the new bar, they're removed from the old one. The code in Listing 7, although it looks similar, doesn't work; the loop terminates early:
Listing 7. Loop terminating too soon
// DOES NOT WORK
for (i = 0; i < oldBar.getMenuCount(); ++i) {
if (i == index)
newBar.add(menu);
newBar.add((JMenu) oldMenus[i]);
}
|
The loop in Listing 7 copies only half of the items. For instance, if four items are on the menu bar to begin with, it copies the first two. After it copies the first one, i is 1 and getMenuCount() returns 3; after it copies the second, i is 2 and getMenuCount() returns 2, so the loop ends. I couldn't find any documentation of the "feature" whereby adding a menu to one bar removes it from another, so it might not be intentional. Still, it's easy enough to work around.
Removing a menu from a menu bar is a little easier; just copy all of the other menus over from the old bar to the new one, and you're done. Easy!
If your interface uses a lot of dynamic menu updates, it might be better to create a set of menu bars and switch between them, rather than updating them on the fly all the time. However, if you're changing menus that much, you are probably also driving your users absolutely crazy.
Errata: During the drafting of this article, I overlooked the list of inherited methods of the JMenuBar class. In fact, it has both remove and add methods available that can remove or insert at a specified index. The additional lesson is: Check the inherited methods, not just the class-specific methods.
It's a great blessing that, for the most part, window resizing happens automatically. But you need to take a few implications of resizing into account. Button bars, menu bars, and similar features can become problematic in a very small window. Graphical panels that your program manages itself need to respond to resize events. Let Swing handle packing UI elements, but keep an eye on the size of the component; don't just get the dimensions once and keep using those values.
More subtly, some design decisions, such as frequency of ticks on sliders, might reasonably be updated in response to window resize events. A slider 100 pixels wide can't have as many readable labels as a slider 400 pixels wide. You might want to take some of your UIs even further by adding whole new convenience features on larger displays.
Nonetheless, for the most part, you can ignore window resizing. What you should not do is prevent or override it unnecessarily. Marginal convenience in your layout code is not a necessity. A minimum window size may be justifiable, but let people make windows as large as they want.
The Swing toolkit offers a great deal of flexibility in UI design. Used carefully, the option of updating an interface on the fly can simplify that interface dramatically; presenting a menu only when its options apply, for instance, can be easier for users.
Unfortunately, some of the API features that make this approach possible are a little quirky, and the side effects and interactions are not always as well documented as you might like. If you have an idea for a dynamic interface, be ready to spend a bit of extra time on debugging. You might well be operating out in the corners of the Swing library and find yourself needing to work around surprising behaviors and/or bugs.
Don't let the lack of an obvious implementation discourage you. As this article's JMenuBar example shows, even when there's no support for a task in the API, you might be able to implement it yourself, albeit a bit indirectly.
Try not to go overboard. Dynamic UIs are at their best when they make inherent restrictions clearer to the user. Ideally, a user might not even notice that an interface is changing. If the only time they can use a program's Object menu is when they have an object selected, they won't mind that the menu isn't there the rest of the time.
On the other hand, if a possibility exists that the user can't guess why an option isn't available, it might be better to let the user attempt an action and get an informative error message. This is particularly important for some actions. If the save option is disabled, that doesn't help much if I want to save my data. The program probably thinks it's already saved, but why not let me save it anyway? And if there's a specific reason why I can't save the file, I probably want to know what it is.
Interface design, despite years of study, is still in many ways a young field. Experiment a little. Dynamic changes to UIs can be a wonderful feature that makes them clearer, simpler, and more responsive. Adding dynamic UI features requires anything from a few minutes' work to a substantial time commitment.
Learn
- "Introduction to Swing" and "Intermediate Swing" (Michael Abernethy, developerWorks, June 2005): Get hands-on with Swing in this tutorial series.
- SwingSet demo: Check out an online demo of the original SwingSet2 application.
- "Programming a Dynamic User Interface" (Sun Developer Network, Blum et al., March 1998): Back when AWT was the state of the art in Java UI code, Sun published this excellent article.
- "Dynamic user interface is only skin deep" (Jason Briggs, JavaWorld, May 2000): Skins are one particular variety of dynamic UI.
- "Advanced Synth" (Michael Abernethy, developerWorks, February 2005): Build skinnable apps with Swing's Synth look and feel.
- developerWorks Java technology zone: Hundreds more Java technology resources.
Discuss
- Client-side Java discussion forum: John Zukowski moderates this discussion on all aspects of client-side development, including UIs.







