Previous month:
May 2012
Next month:
September 2012

August 2012

How to approximate BorderLayout with GridBagLayout or BoxLayout

I recently encountered some challenges using Java 6 Swing and AWT components to develop a GUI with a top-level BorderLayout, which did not prevent the shrinking of components in its CENTER area in response to an expanding JList in its WEST area. I provided more context about this problem (and its solution) in my last post, in which I described How to Prevent a JList in a JScrollPane on a JPanel from Resizing. As I mentioned in that post, one way I attempted to resolve the problem was to substitute a different LayoutManager, first GridBagLayout and then BoxLayout. Neither of those substitutions resolved the problem, but in exploring this trajectory, I learned how to approximate the 5 areas of BorderLayout - NORTH, WEST, CENTER, EAST and SOUTH - using GridBagLayout and BoxLayout, and wanted to share simplified versions of these approximations here, in case they may be of use to others.

BorderLayout

My simplified base example, SimpleBorderLayoutDemo, creates a 320x160 pixel GUI with a JButton in each of the 5 areas:

SimpleBorderLayoutDemo_screenshot1

The "north" and "south" buttons are stretched to fill the entire width of the window. This is the case regardless of whether one extends JButton and overrides the getPreferredSize() or getMaximumSize() methods, or uses setPreferredSize() or setMaximumSize(). More specifically, BorderLayout ignores preferred and maximum widths for components in the NORTH and SOUTH areas. Note also that the "west" and "east" buttons are stretched vertically to fill the space between the NORTH and SOUTH areas. This is because BorderLayout ignores the preferred and maximum heights for components in the WEST and EAST areas. Just for completeness, it ignores all preferred and maximum dimensions (width and height) for components added to the CENTER pane.

To get buttons that have the preferred widths and heights, each JButton can be added to a separate JPanel, which has a default FlowLayout (a LayoutManager which respects the preferred heights and widths of its components), and then each of those JPanels can be added to the main contentPane. Using separate JPanels for each JButton results in the following GUI (SimpleBorderLayoutDemo2):

SimpleBorderLayoutDemo2_screenshot1


/**
 * SimpleBorderLayoutDemo2.java
 * 
 * Joe McCarthy, 26 August 2012
 * 
 * Baseline BorderLayout GUI, with extra layer of panels
 * Used to compare with GridBagLayout and BoxLayout
 */

import java.awt.BorderLayout;
import java.awt.Dimension;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class SimpleBorderLayoutDemo2 extends JFrame {
	
	public SimpleBorderLayoutDemo2() {
		// create a button to place in each section
		JButton northButton = new JButton("north");
		JButton westButton = new JButton("west");
		JButton centerButton = new JButton("center");
		JButton eastButton = new JButton("east");
		JButton southButton = new JButton("south");

		// use JPanels (with default FlowLayouts) for each button 
		// so they use preferred sizes
		JPanel northPanel = new JPanel();
		northPanel.add(northButton);
		JPanel westPanel = new JPanel();
		westPanel.add(westButton);
		JPanel centerPanel = new JPanel();
		centerPanel.add(centerButton);
		JPanel eastPanel = new JPanel();
		eastPanel.add(eastButton);
		JPanel southPanel = new JPanel();
		southPanel.add(southButton);

		// add the sections to a JPanel
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BorderLayout());
		mainPanel.add(northPanel, BorderLayout.NORTH);
		mainPanel.add(westPanel, BorderLayout.WEST);
		mainPanel.add(centerPanel, BorderLayout.CENTER);
		mainPanel.add(eastPanel, BorderLayout.EAST);
		mainPanel.add(southPanel, BorderLayout.SOUTH);
		
		// add the JPanel to the main contentPane & make visible
		getContentPane().add(mainPanel);
		setTitle("SimpleBorderLayoutDemo2");
		setPreferredSize(new Dimension(320, 160));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}

	public static void main(String[] args) {
		new SimpleBorderLayoutDemo2();
	}

}

GridBagLayout

To approximate this 5-area layout using GridBagLayout, we would need to use 3 rows and 3 columns, in which the north and south components each consumed 3 columns. We can use GridBagConstraints fields gridx and gridy to specify rows and columns, and use the gridwidth field to enable the north and south components to occupy all 3 columns of the top and bottom rows.

GridBagLayoutForBorderLayoutDemo_screenshot0

GridBagLayout components tend to gravitate toward the middle; in order to spread them out. we can insert Insets specifying the number of pixels to use for padding above, to the left of, below and the to right of each component. If we create a new Insets object initialized with values of 10 for each its edges (top, left, bottom and right), and assign it to the GridBagConstraints insets field for the GridBagLayout, we get the following layout:

GridBagLayoutForBorderLayoutDemo_screenshot1


/**
 * GridBagLayoutForBorderLayoutDemo.java
 * 
 * Joe McCarthy
 * 26 August 2012
 * 
 * Demonstration of using GridBagLayout to approximate BorderLayout
 */

import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class GridBagLayoutForBorderLayoutDemo extends JFrame {
	
	public GridBagLayoutForBorderLayoutDemo() {
		// create a button to place in each section
		JButton northButton = new JButton("north");
		JButton westButton = new JButton("west");
		JButton centerButton = new JButton("center");
		JButton eastButton = new JButton("east");
		JButton southButton = new JButton("south");
		
		// create a single GridBagConstraints variable 
		// (should really use separate variables for each section)
		GridBagConstraints c = new GridBagConstraints();
		// insert Insets to space components out
		c.insets = new Insets(10, 10, 10, 10);
		
		// add the buttons to a JPanel, using contraints
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new GridBagLayout());
		c.gridx = 0;
		c.gridy = 0;
		c.gridwidth = 3;
		c.anchor = GridBagConstraints.NORTH; 	// or PAGE_START
		mainPanel.add(northButton, c);
		c.gridx = 0;
		c.gridy = 1;
		c.gridwidth = 1;
		c.anchor = GridBagConstraints.WEST; 	// or LINE_START
		mainPanel.add(westButton, c);
		c.gridx = 1;
		c.gridy = 1;
		c.gridwidth = 1;
		c.anchor = GridBagConstraints.CENTER;	// default
		mainPanel.add(centerButton, c);
		c.gridx = 2;
		c.gridy = 1;
		c.gridwidth = 1;
		c.anchor = GridBagConstraints.EAST; 	// or LINE_END
		mainPanel.add(eastButton, c);
		c.gridx = 0;
		c.gridy = 2;
		c.gridwidth = 3;
		c.anchor = GridBagConstraints.SOUTH;	// or PAGE_END
		mainPanel.add(southButton, c);
		
		// add the JPanel to main contentPane & make visible
		getContentPane().add(mainPanel);
		setTitle("GridBagLayoutForBorderLayoutDemo");
		setPreferredSize(new Dimension(320, 160));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}

	public static void main(String[] args) {
		new GridBagLayoutForBorderLayoutDemo();
	}

}

BoxLayout

BoxLayout can also be used to approximate the BorderLayout. BoxLayout allows components to be laid out horizontally (along the X_AXIS) or vertically (along the Y_AXIS). In horizontal arrangements, BoxLayout attempts to respect the preferred widths of compoenents; in vertical arrangements, BoxLayout attempts to respect the preferred heights of components.

We could create a top-level BoxLayout with vertical arrangement, in which case (assuming the default top-to-bottom orientation), we would

  • add the north button
  • create and add another JPanel having a BoxLayout with vertical orientation to which we would add west, center and east buttons
  • add the south button

BoxLayoutForBorderLayoutDemo_screenshot0

Alternately, we could create a top-level BoxLayout with horizontal arrangement, in which case (assuming the default left-to-right orientation), we would

  • add the west button
  • create and add another JPanel having a BoxLayout with vertical orientation to which we would add north, center and south buttons
  • add the east button

BoxLayoutForBorderLayoutDemo2_screenshot0

In the first BoxLayout configuration, everything is shifted toward the top, and the north and south buttons appear off-center; in the second BoxLayout configuration, everything seems shifted to the left. These misalignments are due to the default alignments in a BoxLayout. There is a whole section of the Java tutorial on How to Use BoxLayout devoted to Fixing Alignment Problems, so I won't go into the full details here.

To fix the problems above, it is necessary to use setAlignmentX(CENTER_ALIGNMENT) for the north and south buttons in the first BoxLayout.

BoxLayoutForBorderLayoutDemo_screenshot0b

For the second BoxLayout, we would use setAlignmentY(CENTER_ALIGNMENT) for the west and east buttons.

BoxLayoutForBorderLayoutDemo2_screenshot0

The same tutorial also describes Using Invisible Components as Filler, which includes a few different internal padding options. A flexible "glue" filler can be used to add as much filler as necessary to evenly space out the components, in the horizontal or vertical dimensions. So, in addition to alignment adjustments, we'll also want to createHorizontalGlue() between the north and center buttons, and between the center and south buttons, in the first BoxLayout, and then createVerticalGlue() between the west button and the center panel (containing north, center and south buttons), and between the center panel and the east button. In the second BoxLayout, we'd do the opposite, using createVerticalGlue() between the west and center buttons, and between the center and east buttons, and then using createHorizontalGlue() between the north button and center panel, and between the center panel and the south button. Whichever combination we use, we end up with the following layout:

BoxLayoutForBorderLayoutDemo_screenshot1


/**
 * BoxLayoutForBorderLayoutDemo.java
 * 
 * Joe McCarthy
 * 26 August 2012
 * 
 * Demonstration of using BoxLayout to approximate BorderLayout
 */

import java.awt.Dimension;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class BoxLayoutForBorderLayoutDemo extends JFrame {
	
	public BoxLayoutForBorderLayoutDemo() {
		// create a button to place in each section
		JButton northButton = new JButton("north");
		JButton westButton = new JButton("west");
		JButton centerButton = new JButton("center");
		JButton eastButton = new JButton("east");
		JButton southButton = new JButton("south");
		
		// create a center panel for east, center & west using BoxLayout.X_AXIS
		// insert some "horizontal glue" to space them out evenly
		JPanel centerPanel = new JPanel();
		centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.X_AXIS));
		centerPanel.add(westButton);
		centerPanel.add(Box.createHorizontalGlue());
		centerPanel.add(centerButton);
		centerPanel.add(Box.createHorizontalGlue());
		centerPanel.add(eastButton);
		
		// adjust vertical (X_AXIS) alignments of north and south components
		northButton.setAlignmentX(CENTER_ALIGNMENT);
		southButton.setAlignmentX(CENTER_ALIGNMENT);

		// add the north button, center panel and south button to a JPanel
		// insert some "vertical glue" to space them out evenly
		JPanel mainPanel = new JPanel();
		mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
		mainPanel.add(northButton);
		mainPanel.add(Box.createVerticalGlue());
		mainPanel.add(centerPanel);
		mainPanel.add(Box.createVerticalGlue());
		mainPanel.add(southButton);
		
		// add the JPanel to main contentPane & make  visible
		getContentPane().add(mainPanel);
		setTitle("BoxLayoutForBorderLayoutDemo");
		setPreferredSize(new Dimension(320, 160));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}

	public static void main(String[] args) {
		new BoxLayoutForBorderLayoutDemo();
	}

}

Note that each of the final layouts above has some subtle differences; in BorderLayout, the buttons in the WEST, CENTER and EAST are all aligned at the top rather the center of their respective areas; in GridBagLayout, the spaces between the buttons are all very uniform; in BoxLayout, the buttons are pushed out toward the edges. All of these can be modified with further adjustments, but my main goal here was simply to demonstrate how the basic NORTH, WEST, CENTER, EAST and SOUTH areas of a BorderLayout can be approximated using other LayoutManagers.

Potential Problems with Growing and Shrinking Components

Just to round things out, in view of the previous post on preventing the resizing of components, I'll include variations on each of the layouts above where the label of the "west" button is changed to "westwestwest" (3x), "westwestwestwestwest" (5x) and "westwestwestwestwestwestwest" (7x):

BorderLayout (version 1)

SimpleBorderLayoutDemo_screenshot2   SimpleBorderLayoutDemo_screenshot3   SimpleBorderLayoutDemo_screenshot4

In the first case, the EAST area is shrunk to the preferred size of its button; in the second case, the CENTER area is shrunk below the preferred size of its button; in the third case, the CENTER area is entirely consumed by the WEST area.

BorderLayout (version 2)

SimpleBorderLayoutDemo2_screenshot2   SimpleBorderLayoutDemo2_screenshot3   SimpleBorderLayoutDemo2_screenshot4

The same pattern of shrinking and eventual elimination of the CENTER area is repeated.

GridBagLayout

GridBagLayoutForBorderLayoutDemo_screenshot2   GridBagLayoutForBorderLayoutDemo_screenshot3   GridBagLayoutForBorderLayoutDemo_screenshot4

In the GridBagLayout, the expanding "west*" button simply pushes the components to its right further to the right, but their sizes are not changed.

BoxLayout (version 1)

BoxLayoutForBorderLayoutDemo_screenshot2   BoxLayoutForBorderLayoutDemo_screenshot3   BoxLayoutForBorderLayoutDemo_screenshot4

In the first version of BoxLayout, where the center row is a separate JPanel with a BoxLayout.X_AXIS arrangement, we see a pattern similar to that of GridBagLayout, where the expanding "west*" button keeps pushing the other buttons further to the right, without changing their sizes.

BoxLayout (version 2)

BoxLayoutForBorderLayoutDemo2_screenshot2   BoxLayoutForBorderLayoutDemo2_screenshot3   BoxLayoutForBorderLayoutDemo2_screenshot4

A similar pattern can be seen in version 2 of BoxLayout, where the center column is a separate JPanel with a BoxLayout.Y_AXIS arrangement, except that in this case the entire center column is pushed further to the right.

The solution to the problem of expanding components resulting in the shrinking or elimination of other components, as I mentioned in the previous post, is to use setPreferredSize() and/or setMaximumSize() - depending on the LayoutManager - for any components that might expand dynamically during execution and encroach upon other components ... such as a JList that accepts new String elements that may be longer than any previous element.


How to Prevent a JList in a JScrollPane on a JPanel from Resizing

I've been working on a graphical user interface to enable a user to view, modify and add relevance judgments for a set of results returned by a search engine in response to a set of topics. The GUI was developed as part of some work I've been doing on the Text REtrieval Conference (TREC) 2012 Medical Records Track. I hope to write more about the GUI and the work on TREC in the future. For now, I want to share a solution I developed to a problem with the Java 6 Swing and AWT components I was struggling with over the past few days.

The problem was a JList contained within a JScrollPane contained within a JPanel that was resizing when new String elements added to the JList were longer than its current width. I initially implemented the GUI, which extends JFrame, using a BorderLayout:

BorderLayoutExample

The offending JList was in the West (leftmost) area, and when it grew (after adding a string longer than its initial width), it shrank the JTextArea component I have in the Center area. I wanted to prevent any resizing from happening.

I read that the Center area of a BorderLayout cannot be protected from resizing - it fills whatever space is available after all the other components have been rendered - so I experimented with different LayoutManagers (GridBagLayout and BoxLayout) ... which will be the subject of a future blog post [update: see How to approximate BorderLayout with GridBagLayout or BoxLayout]. I couldn't manage to get GridBagLayout to work and play nicely with my components; BoxLayout - using a BoxLayout.X_AXIS JPanel for the West, Center and East area components, which was contained inside a BoxLayout.Y_AXIS JPanel (which was wedged between BoxLayout.Y_AXIS JPanels for North and South areas) - reduced the shrinking, but did not eliminate it.

I considered extending JPanel and overriding the getPreferredSize() or getMaximumSize() methods, but decided against this as the log files revealed that some resizing occurs after all the components in the GUI are initially populated. I wanted to allow the component sizes to settle into an initial populated state, and then prevent subsequent resizing.

After trying several other possibilities, I finally wrote a method to update the width associated with the PreferrredSize and MaximumSize, which I call after populating the components. I'll share it below, in case it is of use to others, or in case others have better solutions.

private void restrictPanelWidth(JPanel panel) {
	int panelCurrentWidth = panel.getWidth();
	int panelPreferredHeight = (int)panel.getPreferredSize().getHeight();
	int panelMaximumHeight = (int)panel.getMaximumSize().getHeight();
	panel.setPreferredSize(new Dimension(panelCurrentWidth, panelPreferredHeight));	
	panel.setMaximumSize(new Dimension(panelCurrentWidth, panelMaximumHeight));
}

Among the shortcomings of this solution is that any JPanel restricted by this method will not automatically resize if/when the containing window (JFrame) is resized. My expectation is that my GUI will fill the screen - however large that screen is - and that users will generally not want to make it any smaller than full screen. If my assumption proves unwarranted, I suppose I could reintroduce some flexibility via the componentResized() method of a ComponentAdapter, but I'm going to wait to see if any of the [currently] small group of users asks for this capability.

Update: I discovered another possible approach, which I have not now tried [see update 2 below], described in the Advanced JList Programming article at the Sun Developer Network (SDN). In the section entitled "JList Performance: Fixed Size Cells, Fast Renderers", Hans Muller suggests using the setFixedCellWidth() or setPrototypeCellValue() - which is passed a prototype (e.g., String) value that sets the cell width and cell height to the width and height of the prototype value - method to restrict the width of cells in a JList. He also notes "be sure to set the prototypeCellValue property after setting the cell renderer". The shortcoming I envision with this approach is that I would have to call setFixedCellWidth() any time that the window is resized ... though, as noted before, this is a scenario I don't currently handle in the GUI anyway.

Update 2: When I loaded the GUI with a JList containing cells wider than the desired width, the original problem recurred, so my fix was not effective. I have since used setFixedCellWidth() for the two JLists in the West and East areas, and all is well in my [GUI] world again.