ORIGINAL DRAFT
It seems like a status bar component would have been an obvious addition to the Swing collection. At first, I was a little perplexed when this simple component failed to show up. Then it seemed that, maybe, a JStatusBar component might be unnecessary. After all, it’s not something you can’t implement easily with a simple JPanel and a FlowLayout. But when you get down to developing a serious application, it seems to be a missing piece that quickly turns into a series of hard-coded solutions rather than a solid reusable component you can keep handy in your bag of tricks. We’ll solve that problem this month with a flexible JStatusBar class that you can put to good use.
JStatusBar allows you to define regions with a custom layout manager named, appropriately enough, StatusLayout. Typically, you’ll want to specify labels or other components with specific dimensions, and have other components take up remaining space. What makes the problem interesting is that the space taken up by expanding components may be need to be relative. For example, if you have a larger area for long text messages and a shorter one for context-based status, you want to be able to tell the layout manager that one of the flexible size elements is twice as big as the other. StatusLayout lets you tell it the fixed size of a component, derive dimensions from the component’s preferred size or take up remaining space relative to other variable-size components.
Figure 1 shows an example JStatusBar in the SOUTH part of a BorderLayout. The areas labeled “Relative” use relative sizing and adjust, based on the over all area to be consumed. The “Relative x2” area is twice as large as the “Relative x1” areas. The “Preferred” area uses the component’s preferred size and the Fixed area uses a value passed in as a constraint in our StatusLayout manager. The JProgressBar to the right is scaled with a specified preferred size as well as a black LineBorder and surrounding EmptyBorder to space things out.
To control the way components are handled in StatusLayout, we’ll use a constraint object called StatusConstraint which implement an StatusArea interface. The interface is simple and includes only two calls:
public interface StatusArea
{
public boolean isRelativeWidth();
public float getRequiredWidth(Component component);
}
If the isRelativeWidth method returns true, we’ll adjust the size of the component based on available space. The getRequiredWidth method passes the component for which the size is being calculated, so that you can control adjustments. The StatusConstraint implementation returns a relative value if the isRelativeWidth flag is set, otherwise it returns a fixed width, potentially based on the preferred size of the component. Using these constraints with JStatusBar is a simple matter is creating an instance and passing it as the second argument in the add method. JStatusBar supports a set of addElement methods which let you add components without having to work with StatusConstraint objects directly.
Figure 2 shows the class relationships we’ll be using. We’ll take a look at the StatusConstraint, StatusLayout and JStatusBar classes directly. You can find the whole collection online at www.java-pro.com. If you read the column regularly, you’ll know that AbstractLayout comes up anytime we’re working on a layout manager. It exists primarily to provide default behavior for layout managers, leaving only three methods to implement in a subclass: minimumLayoutSize, preferredLayoutSize and layoutContainer. We’ll also be using a simple StatusConstants interface to make the code more readable.
public interface StatusConstants
{
public static final boolean RELATIVE = true;
public static final boolean FIXED = false;
}
Listing 1 shows the source code for StatusConstraint. The class exists primarily to encapsulate the floating point width value and the relative boolean flag, which is set using the StatusConstant values so you can readily see what’s going on. There are four constructors which take either none, one or the other, or both arguments, allowing various combination to be created easily. StatusConstraint implements the StatusArea interface and returns the relative boolean value when isRelativeWidth is called. The getRequiredWidth method is only slightly more interesting, returning a width value if the relative constraint is true or if the specified width is not 1. Otherwise, we return the component’s preferred size. If the setting is relative, the width value is later normalized and proportionally allocated by StatusLayout.
The real work is done in the StatusLayout manager, which you can look at in Listing 2. The minimumLayoutSize and preferredLayoutSize methods are fairly simple. They take the insets into account, sum up the width dimension and use the largest height value, using the minimum and preferred component size, respectively. The layoutContainer method does the tricky stuff. First we calculate to sum of all fixed width sizes, along with the total width for relative components. The relative values are arbitrary and component space is divided by this total when the layout is executed. The second loop reshapes each component based on whether it is relative or not, using a call to getRequiredWidth to determine the actual width if it is. We account for components with no associated constrain by simply using their preferred size.
StatusConstraint objects are associated with components by using a hash table. We intercept the addLayoutComponent and removeLayoutComponent methods in the layout manager to handle this explicitly. Implementing this method does not change the Container behavior when components are added or removed, so we don’t have to do anything special to deal with the standard add and remove methods. If this was an issue, we would expect the Container getComponentCount and getComponent methods to returning unexpected values. To make it easy to diagnose problems we throw an IllegalArgumentException if the constraint object is not an instance of AreaConstraint.
The JStatusBar component is quite simple, with the StatusLayout accounting for most of the work. You can see the code in Listing 3. The constructor merely sets the layout manager with a vertical and horizontal gap of 2 and a CompoundBorder with an outer ThreeDBorder and an inter EmptyBorder for spacing. The ThreeDBorder is covered in a previous JBorder article and draws a single-pixel, raised bevel by default. The borders are there for purely esthetic reasons and can be set to anything you want by using setBorder on a JStatusBar instance. You can also override the 2 pixel separation between elements by using the setLayout method with a different StatusLayout instance.
While you can certainly use the add method in JStatusBar, you have to be sure you are using an appropriate StatusConstaint object. To make things a little easier, we provide addElement methods that mimic directly the arguments available in the StatusConstraint constructors. You can use these to add components without having to worry about creating your own StatusConstraint objects. Because we use a StatusArea interface, you can even implement your own constraint handlers or make custom components implement the interface, passing the same reference to both arguments in the add method.
When you look for the code online, you’ll find a StatusContainer class which lets you frame a JComponent with a sunken, single-pixel ThreeDBorder. I find it easier to do this with certain components, like a JButton which relies heavily on the border for its own look-and-feel, but this is also convenient when you expect to change components on-the-fly. You may use a JProgressBar interchangeably with a JLabel, for example, to show the user what’s going on in different contexts. StatusLayout provides no control over the order in which components are organized, other than the order in which they were added. By using something like StatusContainer, you can change the contained component without affecting this order. Just reset the child component reference and call revalidate to force the layout to redisplay everything.
JStatusBar is not a very complicated component, but it has a great deal of value when you’re working with Swing applications. It’s easy enough to build your own variant every time, but this defeats the purpose of reusability. I hope you like JStatusBar and use it to your hearts content, leaving you with one less problem to worry about while you’re working on tomorrow’s killer-app.
Listing 1
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
public class StatusConstraint
implements StatusArea, StatusConstants
{
protected boolean relative;
protected float width;
public StatusConstraint()
{
this(FIXED, 1);
}
public StatusConstraint(int width)
{
this(RELATIVE, width);
}
public StatusConstraint(boolean relative)
{
this(relative, 1);
}
public StatusConstraint(boolean relative, float width)
{
this.relative = relative;
this.width = width;
}
public boolean isRelativeWidth()
{
return relative;
}
public float getRequiredWidth(Component component)
{
if (relative || width != 1) return width;
return component.getPreferredSize().width;
}
}
Listing 2
import java.awt.*;
import java.util.*;
public class StatusLayout extends AbstractLayout
{
protected Hashtable table = new Hashtable();
public StatusLayout() {}
public StatusLayout(int hgap, int vgap)
{
super(hgap, vgap);
}
public void addLayoutComponent(Component comp, Object constraints)
{
if (constraints instanceof StatusConstraint)
{
table.put(comp, constraints);
}
else throw new IllegalArgumentException(
"Constraint parameter must be of type LayoutConstraint");
}
public void removeLayoutComponent(Component comp)
{
table.remove(comp);
}
public Dimension minimumLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
int count = parent.getComponentCount();
int width = 0;
int height = 0;
for (int i = 0; i < count; i++)
{
if (i > 0) width += hgap;
Component child = parent.getComponent(i);
Dimension size = child.getMinimumSize();
width += size.width;
if (size.height > height)
height = size.height;
}
return new Dimension(
width + insets.left + insets.right,
height + insets.top + insets.bottom);
}
public Dimension preferredLayoutSize(Container parent)
{
Insets insets = parent.getInsets();
int count = parent.getComponentCount();
int width = 0;
int height = 0;
for (int i = 0; i < count; i++)
{
if (i > 0) width += hgap;
Component child = parent.getComponent(i);
Dimension size = child.getPreferredSize();
width += size.width;
if (size.height > height)
height = size.height;
}
return new Dimension(
width + insets.left + insets.right,
height + insets.top + insets.bottom);
}
public void layoutContainer(Container parent)
{
Insets insets = parent.getInsets();
int count = parent.getComponentCount();
int gaps = hgap * ((count > 0) ? count - 1 : 0);
// Count fixed width and relative total
int fixedWidth = 0;
float relativeTotal = 0;
for (int i = 0; i < count; i++)
{
Component child = parent.getComponent(i);
Dimension size = child.getPreferredSize();
if (table.containsKey(child))
{
StatusConstraint constraint = (StatusConstraint)table.get(child);
if (!constraint.isRelativeWidth())
{
fixedWidth += (int)constraint.getRequiredWidth(child);
}
else
{
relativeTotal += constraint.getRequiredWidth(child);
}
}
else fixedWidth += child.getPreferredSize().width;
}
int availableWidth = parent.getSize().width -
fixedWidth - gaps - (insets.left + insets.right);
// Now do the layout
int position = insets.left;
int height = parent.getSize().height -
(insets.top + insets.bottom);
int width;
for (int i = 0; i < count; i++)
{
Component child = parent.getComponent(i);
Dimension size = child.getPreferredSize();
if (table.containsKey(child))
{
StatusConstraint constraint = (StatusConstraint)table.get(child);
if (!constraint.isRelativeWidth())
{
width = (int)constraint.getRequiredWidth(child);
}
else
{
float required = constraint.getRequiredWidth(child);
width = (int)(required / relativeTotal * availableWidth);
}
}
else
{
width = child.getPreferredSize().width;
}
child.setBounds(position, insets.top, width, height);
position += width + hgap;
}
}
}
Listing 3
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
public class JStatusBar extends JPanel
implements StatusConstants
{
public JStatusBar()
{
setLayout(new StatusLayout(2, 2));
setBorder(new CompoundBorder(new ThreeDBorder(),
new EmptyBorder(2, 2, 2, 2)));
}
public void addElement(Component component)
{
add(component, new StatusConstraint());
}
public void addElement(Component component, int width)
{
add(component, new StatusConstraint(width));
}
public void addElement(Component component, boolean relative)
{
add(component, new StatusConstraint(relative));
}
public void addElement(Component component,
boolean relative, float width)
{
add(component, new StatusConstraint(relative, width));
}
}