Building a Drop-Down Menu Button





Building a Drop-Down Menu Button

This hack shows how to build a color chooser as a proper drop-down component. It will behave like JComboBox but without the extension headaches of Sun's version of the class.

Most custom Swing components are created with simple subclasses of the standard base classes in javax.swing. This works fine most of the time, but every now and then you need to build something where there is no easy standard component to start with. Even worse, sometimes the obvious choice for your starting point is a component so convoluted that you can't figure out where to start. Still, you'd rather not reimplement the wheel. No, I'm not talking about JTRee or JTableI'm referring to the JComboBox. It seems like such a simple component, but the implementation is fiendishly complex.

Most large applications use components that feel like the JComboBox, but do something entirely different, like select a color or show a history list. A quick search through the JComboBox API doesn't turn up any obvious extension points. You could customize it with some cell renderers, but if you need a component that doesn't show a list of data, you are pretty much out of luck. The source to JComboBox is not very helpful either. The work is spread out over several UI classes in the various Look and Feel (L&F) packages. If you did customize one of those, your component would look out of place when used in a different L&F. The only real option is to write your own combo box, which is pretty easy except for the actual drop-down part. You need to show a component on top of the others, poking out of the frame occasionally, but without any decorations of its own. It should be just a borderless floating box. Digging through Swing's source code reveals the secret ingredient: a JWindow.

JWindow is a subclass of Window but not of Frame. This means it has no decorations on the side, and it is hidden from the Dock and Taskbar. This is exactly what you want from a pop up. Care must be taken when creating it, however, as you must ensure the window appears only on top of the existing components, and that it disappears when something else gains focus or the window moves. Fortunately, you can do all of this with one composite component and a few event listeners.

DropDownComponent will be a composite of the visible component, a down arrow trigger button, and the hidden component that will appear in the pop up. By thinking of your custom component as a composite of existing components, you can make it very flexible. Additionally, subclasses must be able to add different components to make something new out of the same pieces.

Figure is the start of a DropDownComponent class. It extends JComponent directly and implements both the action and ancestor listener interfaces. It assembles the visible and drop-down components passed into its constructor with an arrow trigger and the listeners.

Skeleton for a drop-down combo box
	public class DropDownComponent extends JComponent 
		implements ActionListener, AncestorListener {
		protected JComponent drop_down_comp;
		protected JComponent visible_comp;
		protected JButton arrow;
		protected JWindow popup;

		public DropDownComponent(JComponent vcomp, JComponent ddcomp) {
		drop_down_comp = ddcomp;
		visible_comp = vcomp;

		arrow = new JButton(new MetalComboBoxIcon());
		Insets insets = arrow.getMargin();
		arrow.setMargin( new Insets( insets.top, 1, insets.bottom, 1 ) );
		arrow.addActionListener(this);
		addAncestorListener(this);
		
		setupLayout(); 
		}
	}

The arrow is just a JButton with a MetalComboBoxIcon. Reusing this arrow lets the code pick up any Metal Look and Feel customizations. The last line of the constructor calls another method in the class, setupLayout(), to position the arrow next to the visible component while letting the component still grow:

	protected void setupLayout() {
		GridBagLayout gbl = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		setLayout(gbl);
		
		c.weightx = 1.0; c.weighty = 1.0;
		c.gridx = 0; c.gridy = 0;
		c.fill = c.BOTH;
		gbl.setConstraints(visible_comp,c);
		add(visible_comp);

		c.weightx = 0;
		c.gridx++;
		gbl.setConstraints(arrow,c);
		add(arrow);

	}

So far, this is all standard Swing code. The tricky part is dealing with the JWindow pop up. The pop up must be positioned right below the visible component and be on top of the screen. You also need to look for lost focus events to know when to hide the pop up again. To handle the pop up, use the actionPerformed() method:

	public void actionPerformed(ActionEvent evt) {
		// build pop-up window
		popup = new JWindow(getFrame(null));
		popup.getContentPane().add(drop_down_comp);
		popup.addWindowFocusListener(new WindowAdapter() {
		
			public void windowLostFocus(WindowEvent evt) { 
				popup.setVisible(false);
			}
		});
		popup.pack();

		// show the pop-up window
		Point pt = visible_comp.getLocationOnScreen();
		pt.translate(0,visible_comp.getHeight());
		popup.setLocation(pt);
		popup.toFront();
		popup.setVisible(true);
		popup.requestFocusInWindow();

	}

The actionPerformed() method will be called whenever the arrow button triggers it. It creates a new JWindow, adds the drop-down child component, positions the window, and then shows it on top of any other components. The JWindow has an anonymous listener that will close the window if it loses focus. Notice that the JWindow constructor takes the result of getFrame(). getFrame() finds the parent frame of the composite drop-down component. The JWindow accepts this frame as its owner, meaning it will be positioned relative to the parent frame and be moved along with it. More importantly, it can receive focus events. Windows without owners can't get focus events because they are effectively out of the focus system. These events are important as they let you know when to hide the window again. Without the frame returned from getFrame(), the pop up would stay visible and stationary, even if the parent frame gets focus or moves. Here's the code for getFrame():

	protected Frame getFrame(Component comp) {
		if(comp == null) {
		
			comp = this;
		}
		if(comp.getParent() instanceof Frame) {
			return (Frame)comp.getParent();
		}
		return getFrame(comp.getParent());
		
		}

With the code so far, you can show the pop-up window. To close it, you must listen for ancestor events to find out when something above the dropdown in the component tree has changed. They all just call hidePopup() to safely turn it off:

	public void ancestorAdded(AncestorEvent event){
		hidePopup();
	}
	
	public void ancestorRemoved(AncestorEvent event){
		hidePopup();
	}

	public void ancestorMoved(AncestorEvent event){
		if (event.getSource() != popup) {
			hidePopup();
		}
	}

	public void hidePopup() {
		if(popup != null && popup.isVisible()) {
			popup.setVisible(false);
		}
	}

Adding a Color Selection Panel

With the DropDownComponent finished, you can finally build something with it. For this hack, I've chosen a color selector. This is a small widget that lets the user pick one of 12 standard colors without having to open up a full color chooser. Most word processors and spreadsheets have a component like this, so there's no reason for Swing not to have one, too.

ColorSelectionPanel, shown in Figure, is just a JPanel with a 4 x 3 grid of buttons. Each button represents one of the most common 10 colors, plus black and white. When a color button is clicked, it will call selectColor() to fire off a color selection event.

A color selection panel to be used in the drop-down component
	public class ColorSelectionPanel extends JPanel {
		public ColorSelectionPanel() {
			GridBagLayout gbl = new GridBagLayout();
			GridBagConstraints c = new GridBagConstraints();
			setLayout(gbl);

			// reusable listener for each button        
			ActionListener color_listener = new ActionListener() { 
				public void actionPerformed(ActionEvent evt) {      
				selectColor(((JButton)evt.getSource()).getBackground()); 
				}
			};

			// set up the standard 12 colors
			Color[] colors = new Color[12];
			colors[0] = Color.white;
			colors[1] = Color.black;
			colors[2] = Color.blue;
			colors[3] = Color.cyan;
			colors[4] = Color.gray;
			colors[5] = Color.green;
			colors[6] = Color.lightGray;
			colors[7] = Color.magenta;
			colors[8] = Color.orange;
			colors[9] = Color.pink;
			colors[10] = Color.red;
			colors[11] = Color.yellow;

			// lay out the grid
			c.gridheight = 1;
			c.gridwidth = 1;
			c.fill = c.NONE;
			c.weightx = 1.0;
			c.weighty = 1.0;
			for(int i=0; i<3; i++) {
				for(int j=0; j<4; j++) {
				c.gridx=j;
				c.gridy=i;
				JButton button = new ColorButton(colors[j+i*4]);
				gbl.setConstraints(button,c);
				add(button);
				button.addActionListener(color_listener);
				}
			}
			
		}
		// fire off a selectedColor property event
		protected Color selectedColor = Color.black;
		public void selectColor(Color newColor) {

			Color oldColor = selectedColor;
			selectedColor = newColor;
			firePropertyChange("selectedColor",oldColor, newColor);
		}

	}

ColorSelectionPanel uses a custom JButton called ColorButton (shown in Figure). It has no text and a small size so that you can fit 12 of them inside the drop-down window. The button's background comes from the color it represents, and the button draws its own border, so there is no need to draw a grid.

Custom JButton for color selection
	public class ColorButton extends JButton {
		public ColorButton(Color col) {
			super();
			this.setText("");
			Dimension dim = new Dimension(15,15);
			this.setSize(dim);
			this.setPreferredSize(dim);
			this.setMinimumSize(dim);
			this.setBorderPainted(true);
			this.setBackground(col);

		}
	}

To put the color selector together, you just need to pack the ColorSelectionPanel and a status button into a DropDownComponent. You also need to add a property change listener to detect when the user has selected a new color and then hide the pop up. This is all handled by Figure.

Assembling a working color selection widget
	public class DropDownTest extends JPanel {
		public static void main(String[] args) {
			final JButton status = new JButton("Color");
			final JPanel panel = new ColorSelectionPanel();
			final DropDownComponent dropdown = new DropDownComponent(status,panel);
			panel.addPropertyChangeListener("selectedColor",

				new PropertyChangeListener() {
				public void propertyChange(PropertyChangeEvent evt) {
				dropdown.hidePopup();                
				status.setBackground((Color)evt.getNewValue());
				}
			});

			JFrame frame = new JFrame("Drop Down Test");
			frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
			frame.getContentPane().setLayout(new BorderLayout(  ));
			frame.getContentPane().add("North",dropdown);
			frame.getContentPane().add("Center",new JLabel("Drop Down Test"));
			frame.pack();
			frame.setSize(300,300);
			frame.show();

		}
	}	

After building the DropDownComponent and putting it in a standard JFrame, your color selector will look like Figure.

A drop-down color chooser in a test JFrame


One nice thing about assembling the drop-down from standard components is that it will still look good when used with a different Look and Feel. Everything the user sees on the screen is some subclass of the standard JButton, but it is just presented in a non-traditional manner. If you switch to another theme where standard buttons are shaped differently, the custom component adapts automatically.


     Python   SQL   Java   php   Perl 
     game development   web development   internet   *nix   graphics   hardware 
     telecommunications   C++ 
     Flash   Active Directory   Windows