★ wanayoo — archive 1999 https://tablelayout.dev.java.net/articles/TableLayoutTutorialPart2/TableLayoutTutorialPart2.htmlNouvelle recherche | Portail wanayoo
Login | Register
My pages Projects Communities java.net

Notice: the projects area will undergo maintenance to enable Project Editor feature Monday, 6/25 from 7:00 pm to 7:45 pm PDT.


Powerful GUIs  
TableLayout Tutorial, Part 2
Building Powerful GUIs
by Daniel Barbalace
06/27/2005

TableLayout Download
The latest version of TableLayout can be downloaded from here. The complete source code for examples given in this article can be found here -- note that the version of TableLayout used by these examples is not necessarily the latest version.

Java Web Start Download
Java Web Start allows you to launch the examples in this article from your web browser. Download Java Web Start from here.

Print
A printable Word document of this article is available here.


Article Table of Contents
 
 Multicell 
 Spanning multiple cells 
 Justifying with multiple cells 
   
 Borders 
 Without status bar 
 With status bar 
 With left menu 
   
 Toggle Visibility 
 Single row 
 Switching between groups of rows 
   
 Scrolling 
 Simple scolling 
 Making an entrance 
   
 Finale 
 Component Orientation 
 Grid class 
 Conclusion 
   

Mutlitcell

Spanning multiple cells

In Part 1 you learned about the concepts behind TableLayout and how to create applications quickly with this layout manager. Now we look at some more advanced features of TableLayout that can be used to create powerful applications with ease.

One of the most common techniques for creating layouts with TableLayout is to have a component span mutliple cells. This can be achieved quite easily with either a String or a TableLayoutConstraints constraint. In this example, we will create the following mockup (figure 1) of the Windows XP networking setup.

The grid that we will use will contain three columns for elements as well as two columns for a border. For clarity, the rows will be created explicitly as they are needed. The second figure shows the rows and columns in the grid.

screen shot screen shot

The textfield spans two columns. The scroll pane (which contains the list of connection items), both labels, and both checkboxes span three columns. This is accomplished by specifying two cells for the constraints. The first cell is the upper-left corner of the area used by the component. The second cell is the lower-right corner. For example, the scoll pane occupies cells (1, 5) through (3, 5). It was added to the container with the line container.add(scrollPane, "1, 5, 3, 5");. The scroll pane could have also been added with the type-safe equivalent line container.add(scrollPane, new TableLayoutConstaints(1, 5, 3, 5));.

Listing 1: Multicell
package example2; import java.awt.*; import javax.swing.*; import java.util.*; import info.clearthought.layout.TableLayout; /** * Example of components spanning cells. * * @author Daniel E. Barbalace * @version 1.0, June 14, 2005 */ public class Multicell { /** * Runs the program. */ public static void main (String args[]) { // Create frame JFrame frame = new JFrame("Local Area Connection - Primary Properties"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new FlowLayout()); // Items to display in list control Vector listElement = new Vector(); listElement.add("Client for Microsoft Networks"); listElement.add("File and Print Sharing for Microsoft Networks"); listElement.add("QoS Packet Scheduler"); listElement.add("Microsoft TCP/IP version 6"); listElement.add("Internet Protocol (TCP/IP"); // Create controls JLabel labelConnect = new JLabel("Connect using:"); JTextField textfieldConnect = new JTextField("Intel(R) PRO/100 VE Network Connection"); JButton buttonConfigure = new JButton("Configure..."); JLabel labelUse = new JLabel("This connection uses the following items:"); JList list = new JList(listElement); JScrollPane scrollPane = new JScrollPane(list); JButton buttonInstall = new JButton("Install"); JButton buttonUninstall = new JButton("Uninstall"); JButton buttonProperties = new JButton("Properties"); JCheckBox checkboxShowIcon = new JCheckBox("Show icon in notification area when connected"); JCheckBox checkboxNotify = new JCheckBox("Notify me when this connection has limited or no connectivity"); JButton buttonOK = new JButton("OK"); JButton buttonCancel = new JButton("Cancel"); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double emptySpace = 10; double [] columnSize = {border, 1.0 / 3.0, TableLayout.FILL, 1.0 / 3.0, border}; double [] rowSize = {border, border}; TableLayout layout = new TableLayout(columnSize, rowSize); layout.setVGap(2); layout.setHGap(5); Container container = frame.getContentPane(); container.setLayout(layout); // Add controls layout.insertRow(1, p); container.add(labelConnect, "1, 1, 3, 1"); layout.insertRow(2, p); container.add(textfieldConnect, "1, 2, 2, 2"); container.add(buttonConfigure, "3, 2"); layout.insertRow(3, emptySpace); layout.insertRow(4, p); container.add(labelUse, "1, 4, 3, 4"); layout.insertRow(5, TableLayout.FILL); container.add(scrollPane, "1, 5, 3, 5"); layout.insertRow(6, p); container.add(buttonInstall, "1, 6"); container.add(buttonUninstall, "2, 6"); container.add(buttonProperties, "3, 6"); layout.insertRow(7, emptySpace); layout.insertRow(8, p); container.add(checkboxShowIcon, "1, 8, 3, 8"); layout.insertRow(9, p); container.add(checkboxNotify, "1, 9, 3, 9"); layout.insertRow(10, emptySpace); layout.insertRow(11, p); container.add(buttonOK, "2, 11"); container.add(buttonCancel, "3, 11"); // Show frame frame.pack(); frame.setVisible(true); frame.toFront(); } }

Justifying with multiple cells

In the previous section we saw how to add a component to more than one cell. By default all components are full justified. Components that span cells can be justified in the same ways that components that occupy a single cell can. In this section we will modify the previous example to justify some of the button controls.

First we give the buttons an explicit preferred size that is independent of their labels, so that they will all be the same size. This is done by calling the setPreferredSize method of JComponent. Second we place the OK and Cancel buttons in a panel so that they can be laid out independently of the other controls. The new panel will contain only two columns and will be just large enough to house the two buttons.

The final change is to add horizontal and vertical justifications to the components' constraints. This is done by changing the lines like container.add(buttonInstall, "1, 6"); to container.add(buttonInstall, "1, 6, LEFT, TOP");. Similiarly, the button panel, which spans three columns, is added with the line container.add(panelButton, "1, 11, 3, 11, RIGHT, TOP");. The justification flags are simply appended to the string constraint.

For those who wish to use type-safe code, the components could also be added with the lines like container.add(buttonInstall, new TableLayoutConstraints(1, 6, 1, 6, LEFT, TOP));. In order to avoid ambiguity, the full cell range must be specified when giving justifications. These changes are highlighted in blue in the following listing.

When using left, center, right, top, bottom, leading, or trailing justification, components will not be made larger than their preferred size. However, they may be made smaller if the area they occupy is not large enough to support their preferred sizes.

Finally, since vertical and horizontal justifications are independent of each other, it is possible to stretch a component in both directions, just one direction, or neither direction. Similarly, a component can be placed in a corner, the center, or an edge of a rectangular area.

Listing 2: Multicell Justification
package example2; import java.awt.*; import javax.swing.*; import java.util.*; import info.clearthought.layout.TableLayout; /** * Example of components spanning cells and using justification. * * @author Daniel E. Barbalace * @version 1.0, June 14, 2005 */ public class MulticellJustify { /** * Runs the program. */ public static void main (String args[]) { // Create frame JFrame frame = new JFrame("Local Area Connection - Primary Properties"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLayout(new FlowLayout()); // Items to display in list control Vector listElement = new Vector(); listElement.add("Client for Microsoft Networks"); listElement.add("File and Print Sharing for Microsoft Networks"); listElement.add("QoS Packet Scheduler"); listElement.add("Microsoft TCP/IP version 6"); listElement.add("Internet Protocol (TCP/IP"); // Create controls JLabel labelConnect = new JLabel("Connect using:"); JTextField textfieldConnect = new JTextField("Intel(R) PRO/100 VE Network Connection"); JButton buttonConfigure = new JButton("Configure..."); JLabel labelUse = new JLabel("This connection uses the following items:"); JList list = new JList(listElement); JScrollPane scrollPane = new JScrollPane(list); JButton buttonInstall = new JButton("Install"); JButton buttonUninstall = new JButton("Uninstall"); JButton buttonProperties = new JButton("Properties"); JCheckBox checkboxShowIcon = new JCheckBox("Show icon in notification area when connected"); JCheckBox checkboxNotify = new JCheckBox("Notify me when this connection has limited or no connectivity"); JButton buttonOK = new JButton("OK"); JButton buttonCancel = new JButton("Cancel"); // Give buttons a definitive preferred size independent of their labels Dimension preferredSize = new Dimension(123, 26); buttonConfigure.setPreferredSize(preferredSize); buttonInstall.setPreferredSize(preferredSize); buttonUninstall.setPreferredSize(preferredSize); buttonProperties.setPreferredSize(preferredSize); buttonOK.setPreferredSize(preferredSize); buttonCancel.setPreferredSize(preferredSize); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double emptySpace = 10; double [] columnSize = {border, 1.0 / 3.0, TableLayout.FILL, 1.0 / 3.0, border}; double [] rowSize = {border, border}; TableLayout layout = new TableLayout(columnSize, rowSize); layout.setVGap(2); layout.setHGap(5); Container container = frame.getContentPane(); container.setLayout(layout); // Place OK and cancel buttons in a panel so that they can be laid out // independently of all other controls JPanel panelButton = new JPanel(); TableLayout layoutPanel = new TableLayout(new double [][] {{p, p}, {p}}); layoutPanel.setHGap(5); panelButton.setLayout(layoutPanel); panelButton.add(buttonOK, "0, 0"); panelButton.add(buttonCancel, "1, 0"); // Add controls layout.insertRow(1, p); container.add(labelConnect, "1, 1, 3, 1"); layout.insertRow(2, p); container.add(textfieldConnect, "1, 2, 2, 2"); container.add(buttonConfigure, "3, 2"); layout.insertRow(3, emptySpace); layout.insertRow(4, p); container.add(labelUse, "1, 4, 3, 4"); layout.insertRow(5, TableLayout.FILL); container.add(scrollPane, "1, 5, 3, 5"); layout.insertRow(6, p); container.add(buttonInstall, "1, 6, LEFT, TOP"); container.add(buttonUninstall, "2, 6, CENTER, TOP"); container.add(buttonProperties, "3, 6, RIGHT, TOP"); layout.insertRow(7, emptySpace); layout.insertRow(8, p); container.add(checkboxShowIcon, "1, 8, 3, 8"); layout.insertRow(9, p); container.add(checkboxNotify, "1, 9, 3, 9"); layout.insertRow(10, emptySpace); layout.insertRow(11, p); container.add(panelButton, "1, 11, 3, 11, RIGHT, TOP"); // Show frame frame.pack(); frame.setVisible(true); frame.toFront(); } }

Borders

Without status bar

One of the most common patterns in layouts is the use of a border. Although some containers use insets, these are typically used only when the container paints its own border as in a group box. Often the application developer wants to add an empty space around a window to make the interface more asthetically pleasing. In this example we show a simple border inside a window used to display web pages. (Please note that since the example jar file is unsigned, you will only be able to load pages from dev.java.net when running the application from Java Web Start.) The application will look like this.

screen shot

The border is created by specifying rows and columns in the layout with the following code. double border = 10; double [] columnSize = {border, p, p, p, TableLayout.FILL, border}; double [] rowSize = {border, p, p, TableLayout.FILL, border}; In this case a uniformed border of 10 pixels is created on all four sides of the window. You can just as easily create a border that varies from side to side just as one can in a word processor.

Listing 3: Simple Border
package example2; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import javax.swing.*; import info.clearthought.layout.*; /** * BorderSimple demonstrates how to create a simple border. * * @author Daniel E. Barbalace * @version 1.0, Jun 17, 2005 */ public class BorderSimple implements ActionListener { /** Main frame */ private JFrame frame; /** Used to display the path of the file being displayed */ private JLabel labelFilename; /** When selected, files will be displayed as web pages */ private JRadioButton radioWebPage; /** When selected, files will be displayed as text */ private JRadioButton radioText; /** Used to display HTML files as web pages */ private JEditorPane paneWeb; /** Used to display HTML files as text */ private JTextPane paneText; /** Used to scroll document view */ private JScrollPane scrollPane; /** Open file menu item */ private JMenuItem menuOpenFile; /** Open URL item */ private JMenuItem menuOpenUrl; /** Exit menu item */ private JMenuItem menuExit; /** * Runs the program. */ public static void main (String args[]) { new BorderSimple(); } /** * Creates the application's GUI. */ public BorderSimple() { // Create frame frame = new JFrame("Simple Border"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Create menu JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("File"); menuOpenFile = menu.add("Open File"); menuOpenUrl = menu.add("Open URL"); menuExit = menu.add("Exit"); menuOpenFile.addActionListener(this); menuOpenUrl.addActionListener(this); menuExit.addActionListener(this); menuBar.add(menu); frame.setJMenuBar(menuBar); // Create controls JLabel labelFile = new JLabel("File:"); labelFilename = new JLabel(""); JLabel labelView = new JLabel("View as:"); radioWebPage = new JRadioButton("Web Page"); radioText = new JRadioButton("Text"); ButtonGroup groupView = new ButtonGroup(); groupView.add(radioWebPage); groupView.add(radioText); radioWebPage.setSelected(true); radioWebPage.addActionListener(this); radioText.addActionListener(this); paneWeb = new JEditorPane(); paneWeb.setEditable(false); paneText = new JTextPane(); paneText.setEditable(false); scrollPane = new JScrollPane(paneWeb); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setPreferredSize(new Dimension(600, 400)); scrollPane.setMinimumSize(new Dimension(10, 10)); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double [] columnSize = {border, p, p, p, TableLayout.FILL, border}; double [] rowSize = {border, p, p, TableLayout.FILL, border}; TableLayout layout = new TableLayout(columnSize, rowSize); layout.setHGap(5); Container container = frame.getContentPane(); container.setLayout(layout); // Add controls container.add(labelFile, "1, 1, RIGHT, BOTTOM"); container.add(labelFilename, "2, 1, 4, 1"); container.add(labelView, "1, 2, RIGHT, CENTER"); container.add(radioWebPage, "2, 2"); container.add(radioText, "3, 2"); container.add(scrollPane, "1, 3, 4, 3"); // Show one html file by default setPath("/sample.html"); // Show frame frame.pack(); frame.setVisible(true); frame.toFront(); } /** * Sets the path of the document to view. * * @param path full or relative path of document */ private void setPath (String path) { // Attempt to open file in jar URL url = BorderSimple.class.getResource(path); // Attempt to open a full URL if (url == null) { try { url = new URL(path); } catch (MalformedURLException e) {} } // Attempt to open a local file if (url == null) { try { File file = new File(path); url = file.toURL(); } catch (MalformedURLException e) {} } // If any of the attempts succeeded, open the url if (url != null) { try { paneWeb.setPage(url); paneText.setText(getContent(url)); labelFilename.setText(path); } catch (IOException e) { paneWeb.setText(""); paneText.setText(""); labelFilename.setText(""); System.err.println("Attempted to read a bad URL: " + url); } } else { System.err.println("Couldn't find file: " + path); } } /** * Gets the contents of a URL as a string. * * @param url document to get * * @return a string containing the URL's contents */ private String getContent (URL url) { StringBuffer content = new StringBuffer(); try { String line = ""; BufferedReader input = new BufferedReader(new InputStreamReader(url.openStream())); while (line != null) { content.append(line); if (line.length() > 0) content.append('\n'); line = input.readLine(); } input.close(); } catch (MalformedURLException me) {} catch (IOException e) {} return content.toString(); } /** * Invoked when one of the radio buttons is selected. */ public void actionPerformed (ActionEvent e) { Object source = e.getSource(); if (source == radioWebPage) scrollPane.setViewportView(paneWeb); else if (source == radioText) scrollPane.setViewportView(paneText); else if (source == menuOpenFile) { JFileChooser fc = new JFileChooser(); int returnVal = fc.showOpenDialog(menuOpenFile); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = fc.getSelectedFile(); setPath(file.getPath()); } } else if (source == menuOpenUrl) { String message = "URL to open"; String s = (String) JOptionPane.showInputDialog (frame, message, "Open URL", JOptionPane.PLAIN_MESSAGE, null, null, null); if ((s != null) && (s.length() > 0)) setPath(s); } else if (source == menuExit) System.exit(0); } }

With status bar

At this point you might wonder why TableLayout does not provide a special method just for creating a border. If all borders were exactly like the one above, it might make sense to have a convenience method for creating them. However, TableLayout allows you to create borders that are far more flexible.

In this example we change the previous application to use a status bar. A status bar is not a standard part of a JFrame like a window menu is. We cannot call a method like JFrame.setJMenuBar to set a status bar. Instead we will create a simple control to render a status bar. Since this control is our own, the JFrame class will not allocate space for it like JFrame does for the menu bar. Therefore, we must include some space for it in our layout.

To make our application look more professional, we will also add a heading, or banner, to the top of the window just below the window menu. As with the status bar, we will allocate some space for the heading in our layout. The end result will look like this.

screen shot

The border is no longer along the edges of the window. The top and bottom border have been moved inward so that they appear between the heading and the status bar. The code changes trivially to accomplish this. double [] rowSize = {p, border, p, p, TableLayout.FILL, border, p}; The border constants are simply moved inward in the array. Such flexibility could not be done through a setBorder method. The changes to the code are highlighted in blue in the following listing.

Listing 4: Border with Status Bar
package example2; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import javax.swing.*; import info.clearthought.layout.*; /** * BorderStatus expands the BorderSimple demonstration by adding a heading and * a status bar. * * @author Daniel E. Barbalace * @version 1.0, Jun 17, 2005 */ public class BorderStatus implements ActionListener { /** Main frame */ private JFrame frame; /** Used to display the path of the file being displayed */ private JLabel labelFilename; /** When selected, files will be displayed as web pages */ private JRadioButton radioWebPage; /** When selected, files will be displayed as text */ private JRadioButton radioText; /** Used to display HTML files as web pages */ private JEditorPane paneWeb; /** Used to display HTML files as text */ private JTextPane paneText; /** Used to scroll document view */ private JScrollPane scrollPane; /** Open file menu item */ private JMenuItem menuOpenFile; /** Open URL item */ private JMenuItem menuOpenUrl; /** Exit menu item */ private JMenuItem menuExit; /** Status bar */ private StatusBar statusBar; /** * Runs the program. */ public static void main (String args[]) { new BorderStatus(); } /** * Creates the application's GUI. */ public BorderStatus() { // Create frame frame = new JFrame("Border with a heading and a status bar"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Create menu JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("File"); menuOpenFile = menu.add("Open File"); menuOpenUrl = menu.add("Open URL"); menuExit = menu.add("Exit"); menuOpenFile.addActionListener(this); menuOpenUrl.addActionListener(this); menuExit.addActionListener(this); menuBar.add(menu); frame.setJMenuBar(menuBar); // Create controls Heading heading = new Heading(); JLabel labelFile = new JLabel("File:"); labelFilename = new JLabel(""); JLabel labelView = new JLabel("View as:"); radioWebPage = new JRadioButton("Web Page"); radioText = new JRadioButton("Text"); ButtonGroup groupView = new ButtonGroup(); groupView.add(radioWebPage); groupView.add(radioText); radioWebPage.setSelected(true); radioWebPage.addActionListener(this); radioText.addActionListener(this); paneWeb = new JEditorPane(); paneWeb.setEditable(false); paneText = new JTextPane(); paneText.setEditable(false); scrollPane = new JScrollPane(paneWeb); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setPreferredSize(new Dimension(600, 400)); scrollPane.setMinimumSize(new Dimension(10, 10)); statusBar = new StatusBar(); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double [] columnSize = {border, p, p, p, TableLayout.FILL, border}; double [] rowSize = {p, border, p, p, TableLayout.FILL, border, p}; TableLayout layout = new TableLayout(columnSize, rowSize); layout.setHGap(5); Container container = frame.getContentPane(); container.setLayout(layout); // Add controls container.add(heading, "0, 0, 5, 0"); container.add(labelFile, "1, 2, RIGHT, BOTTOM"); container.add(labelFilename, "2, 2, 4, 2"); container.add(labelView, "1, 3, RIGHT, CENTER"); container.add(radioWebPage, "2, 3"); container.add(radioText, "3, 3"); container.add(scrollPane, "1, 4, 4, 4"); container.add(statusBar, "0, 6, 5, 6"); // Show one html file by default setPath("/sample.html"); // Show frame frame.pack(); frame.setVisible(true); frame.toFront(); } /** * Sets the path of the document to view. * * @param path full or relative path of document */ private void setPath (String path) { // Attempt to open file in jar URL url = BorderStatus.class.getResource(path); // Attempt to open a full URL if (url == null) { try { url = new URL(path); } catch (MalformedURLException e) {} } // Attempt to open a local file if (url == null) { try { File file = new File(path); url = file.toURL(); } catch (MalformedURLException e) {} } // If any of the attempts succeeded, open the url if (url != null) { try { paneWeb.setPage(url); paneText.setText(getContent(url)); labelFilename.setText(path); statusBar.setText("Loaded " + url); } catch (IOException e) { paneWeb.setText(""); paneText.setText(""); labelFilename.setText(""); statusBar.setText("Attempted to read a bad URL: " + url); } } else { statusBar.setText("Couldn't find file: " + path); } } /** * Gets the contents of a URL as a string. * * @param url document to get * * @return a string containing the URL's contents */ private String getContent (URL url) { StringBuffer content = new StringBuffer(); try { String line = ""; BufferedReader input = new BufferedReader(new InputStreamReader(url.openStream())); while (line != null) { content.append(line); if (line.length() > 0) content.append('\n'); line = input.readLine(); } input.close(); } catch (MalformedURLException me) {} catch (IOException e) {} return content.toString(); } /** * Invoked when one of the radio buttons is selected. */ public void actionPerformed (ActionEvent e) { Object source = e.getSource(); if (source == radioWebPage) scrollPane.setViewportView(paneWeb); else if (source == radioText) scrollPane.setViewportView(paneText); else if (source == menuOpenFile) { JFileChooser fc = new JFileChooser(); int returnVal = fc.showOpenDialog(menuOpenFile); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = fc.getSelectedFile(); setPath(file.getPath()); } } else if (source == menuOpenUrl) { String message = "URL to open"; String s = (String) JOptionPane.showInputDialog (frame, message, "Open URL", JOptionPane.PLAIN_MESSAGE, null, null, null); if ((s != null) && (s.length() > 0)) setPath(s); } else if (source == menuExit) System.exit(0); } /** * A simple graphic to display at the top of the GUI, just below the menu * bar. */ private class Heading extends Component { public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); return new Dimension(400, fm.getHeight() * 3); } public void paint (Graphics g) { Dimension d = getSize(); Graphics2D g2 = (Graphics2D) g; GradientPaint gradient = new GradientPaint (0, 0, new Color(26, 80, 184), d.width, 0, frame.getBackground(), true); g2.setPaint(gradient); g2.fill(new Rectangle(d.width, d.height)); g2.setPaint(Color.white); int x = 0; int i = 0; String [] letter = {"J", "a", "v", "a"}; while (x < d.width) { double offset = (d.height * Math.sin(2 * Math.PI * x / 100) / 4); g2.translate(x, offset); g2.drawString(letter[i++], 0, d.height / 2); g2.translate(-x, -offset); x += 10; if (i == letter.length) i = 0; } x = 0; while (x < d.width) { double offset = (d.height * -Math.sin(2 * Math.PI * x / 100) / 4); g2.translate(x, offset); g2.drawLine(0, d.height / 2, 0, d.height / 2); g2.translate(-x, -offset); x++; } } } /** * A simple status bar. */ private class StatusBar extends Component { private String text = ""; public StatusBar() { setForeground(Color.BLACK); setBackground(Color.LIGHT_GRAY); } public void setText (String text) { if (text != null) { this.text = text; repaint(); } } public String getText() { return text; } public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); int width = fm.stringWidth(text) + 4; int height = fm.getMaxAscent() + fm.getMaxDescent() + 2; return new Dimension(width, height); } public void paint (Graphics g) { FontMetrics fm = getFontMetrics(getFont()); Dimension d = getSize(); int baseline = d.height - fm.getMaxDescent() - 1; g.setColor(getBackground()); g.fillRect(0, 0, d.width, d.height); g.setColor(getForeground()); g.drawString(text, 2, baseline); g.setColor(Color.black); g.drawRect(0, 0, d.width - 1, d.height - 1); } } }

With left menu

In this final example of borders we enhance the application to make it even more professional looking by adding a left navigation menu. Such a layout is common in web applications, but also can prove useful in desktop applications. The left menu is implemented as a JTree that lies to the left of the border. To make the application look stylish, we change the background colors of the JTree to match the heading. The heading is also adjusted so that its fading occurs only to the right of the JTree. The result is shown below.

screen shot

One of the great things about TableLayout is that layouts like the one above can be easily achieved without nesting containers. In this example all controls are added to a single container, the content pane of the application's JFrame. The blue and gray background effects are rendered using the background colors of components placed directly in the content pane. The changes needed to make the left navigation menu are listed in blue below.

Listing 5: Border with Left Menu
package example2; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import info.clearthought.layout.*; /** * BorderLeftMenu extends the BorderStatus demonstration by adding a left * navigation menu. * * @author Daniel E. Barbalace * @version 1.0, Jun 17, 2005 */ public class BorderLeftMenu implements ActionListener, TreeSelectionListener { /** Main frame */ private JFrame frame; /** Used to display the path of the file being displayed */ private JLabel labelFilename; /** When selected, files will be displayed as web pages */ private JRadioButton radioWebPage; /** When selected, files will be displayed as text */ private JRadioButton radioText; /** Used to display HTML files as web pages */ private JEditorPane paneWeb; /** Used to display HTML files as text */ private JTextPane paneText; /** Used to scroll document view */ private JScrollPane scrollPane; /** Open file menu item */ private JMenuItem menuOpenFile; /** Open URL item */ private JMenuItem menuOpenUrl; /** Exit menu item */ private JMenuItem menuExit; /** Status bar */ private StatusBar statusBar; /** Left navigation menu */ private JTree leftMenu; /** * Runs the program. */ public static void main (String args[]) { new BorderLeftMenu(); } /** * Creates the application's GUI. */ public BorderLeftMenu() { // Create frame frame = new JFrame("Border with a heading, status bar, and left menu"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Create menu JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("File"); menuOpenFile = menu.add("Open File"); menuOpenUrl = menu.add("Open URL"); menuExit = menu.add("Exit"); menuOpenFile.addActionListener(this); menuOpenUrl.addActionListener(this); menuExit.addActionListener(this); menuBar.add(menu); frame.setJMenuBar(menuBar); // Create controls Heading heading = new Heading(); JLabel labelFile = new JLabel("File:"); labelFilename = new JLabel(""); JLabel labelView = new JLabel("View as:"); radioWebPage = new JRadioButton("Web Page"); radioText = new JRadioButton("Text"); ButtonGroup groupView = new ButtonGroup(); groupView.add(radioWebPage); groupView.add(radioText); radioWebPage.setSelected(true); radioWebPage.addActionListener(this); radioText.addActionListener(this); paneWeb = new JEditorPane(); paneWeb.setEditable(false); paneText = new JTextPane(); paneText.setEditable(false); scrollPane = new JScrollPane(paneWeb); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setPreferredSize(new Dimension(600, 400)); scrollPane.setMinimumSize(new Dimension(10, 10)); statusBar = new StatusBar(); Vector v = new Vector(); v.add("/sample.html"); v.add("/hypercube.html"); v.add("/itox.html"); v.add("/TableLayout.html"); v.add("/feedback.html"); leftMenu = new JTree(v); leftMenu.addTreeSelectionListener(this); Color backgroundColor = new Color(26, 80, 184); DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); renderer.setTextNonSelectionColor(Color.WHITE); renderer.setBackground(backgroundColor); renderer.setBackgroundNonSelectionColor(backgroundColor); leftMenu.setCellRenderer(renderer); leftMenu.setBackground(backgroundColor); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double [] columnSize = {p, border, border, p, p, p, TableLayout.FILL, border}; double [] rowSize = {p, border, p, p, TableLayout.FILL, border, p}; TableLayout layout = new TableLayout(columnSize, rowSize); layout.setHGap(5); Container container = frame.getContentPane(); container.setLayout(layout); // Add controls container.add(heading, "0, 0, 7, 0"); container.add(labelFile, "3, 2, RIGHT, BOTTOM"); container.add(labelFilename, "4, 2, 6, 2"); container.add(labelView, "3, 3, RIGHT, BOTTOM"); container.add(radioWebPage, "4, 3"); container.add(radioText, "5, 3"); container.add(scrollPane, "3, 4, 6, 4"); container.add(statusBar, "0, 6, 7, 6"); container.add(leftMenu, "0, 1, 0, 5"); // Show one html file by default setPath("/sample.html"); // Show frame frame.pack(); frame.setVisible(true); frame.toFront(); } /** * Sets the path of the document to view. * * @param path full or relative path of document */ private void setPath (String path) { // Attempt to open file in jar URL url = BorderLeftMenu.class.getResource(path); // Attempt to open a full URL if (url == null) { try { url = new URL(path); } catch (MalformedURLException e) {} } // Attempt to open a local file if (url == null) { try { File file = new File(path); url = file.toURL(); } catch (MalformedURLException e) {} } // If any of the attempts succeeded, open the url if (url != null) { try { paneWeb.setPage(url); paneText.setText(getContent(url)); labelFilename.setText(path); statusBar.setText("Loaded " + url); } catch (IOException e) { paneWeb.setText(""); paneText.setText(""); labelFilename.setText(""); statusBar.setText("Attempted to read a bad URL: " + url); } } else { statusBar.setText("Couldn't find file: " + path); } } /** * Gets the contents of a URL as a string. * * @param url document to get * * @return a string containing the URL's contents */ private String getContent (URL url) { StringBuffer content = new StringBuffer(); try { String line = ""; BufferedReader input = new BufferedReader(new InputStreamReader(url.openStream())); while (line != null) { content.append(line); if (line.length() > 0) content.append('\n'); line = input.readLine(); } input.close(); } catch (MalformedURLException me) {} catch (IOException e) {} return content.toString(); } /** * Invoked when one of the radio buttons is selected. */ public void actionPerformed (ActionEvent e) { Object source = e.getSource(); if (source == radioWebPage) scrollPane.setViewportView(paneWeb); else if (source == radioText) scrollPane.setViewportView(paneText); else if (source == menuOpenFile) { JFileChooser fc = new JFileChooser(); int returnVal = fc.showOpenDialog(menuOpenFile); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = fc.getSelectedFile(); setPath(file.getPath()); } } else if (source == menuOpenUrl) { String message = "URL to open"; String s = (String) JOptionPane.showInputDialog (frame, message, "Open URL", JOptionPane.PLAIN_MESSAGE, null, null, null); if ((s != null) && (s.length() > 0)) setPath(s); } else if (source == menuExit) System.exit(0); } /** * Invoked when the value of the left menu changes. */ public void valueChanged (TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); Object [] array = path.getPath(); String item = (array.length == 2) ? "" + array[1] : ""; if (item.length() > 0) setPath(item); } /** * A simple graphic to display at the top of the GUI, just below the menu * bar. */ private class Heading extends Component { public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); return new Dimension(400, fm.getHeight() * 3); } public void paint (Graphics g) { Dimension d = getSize(); Graphics2D g2 = (Graphics2D) g; GradientPaint gradient = new GradientPaint (leftMenu.getSize().width, 0, new Color(26, 80, 184), d.width, 0, frame.getBackground(), false); g2.setPaint(gradient); g2.fill(new Rectangle(d.width, d.height)); g2.setPaint(Color.white); int x = 0; int i = 0; String [] letter = {"J", "a", "v", "a"}; while (x < d.width) { double offset = (d.height * Math.sin(2 * Math.PI * x / 100) / 4); g2.translate(x, offset); g2.drawString(letter[i++], 0, d.height / 2); g2.translate(-x, -offset); x += 10; if (i == letter.length) i = 0; } x = 0; while (x < d.width) { double offset = (d.height * -Math.sin(2 * Math.PI * x / 100) / 4); g2.translate(x, offset); g2.drawLine(0, d.height / 2, 0, d.height / 2); g2.translate(-x, -offset); x++; } } } /** * A simple status bar. */ private class StatusBar extends Component { private String text = ""; public StatusBar() { setForeground(Color.BLACK); setBackground(Color.LIGHT_GRAY); } public void setText (String text) { if (text != null) { this.text = text; repaint(); } } public String getText() { return text; } public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); int width = fm.stringWidth(text) + 4; int height = fm.getMaxAscent() + fm.getMaxDescent() + 2; return new Dimension(width, height); } public void paint (Graphics g) { FontMetrics fm = getFontMetrics(getFont()); Dimension d = getSize(); int baseline = d.height - fm.getMaxDescent() - 1; g.setColor(getBackground()); g.fillRect(0, 0, d.width, d.height); g.setColor(getForeground()); g.drawString(text, 2, baseline); g.setColor(Color.black); g.drawRect(0, 0, d.width - 1, d.height - 1); } } }

Toggle Visibility

Toggle a single row

A simple yet powerful UI technique used in web applications is to toggle the visibility of controls in a form based on the decisions made earlier in the form. This technique is not often used in desktop applications because of the difficulty of making such changes in most layout managers. TableLayout provides an easy means of turning off rows or columns.

Let us go back to the first example given in this article, a network connection property window. There was a checkbox for notifying the user when the Internet connection goes down. Let's add a text field for specifying a timeout that must occur before the notification is sent. We will also put a label next to the text field. The result will look like this.

screen shot

When the checkbox is checked the row containing the new label and textfield is visible because its height is set to TableLayout.PREFFERED. We can effectively remove this row by setting its height to zero when the checkbox is unchecked. This is accomplished with one line of code.

layout.setRow(10, checkboxNotify.isSelected() ? TableLayout.PREFERRED : 0);

However, in order to see this change we need two additional lines of code.

container.invalidate(); container.validate();

The first line tells the AWT framework that the container's layout is now invalid and that it should be laid out again. The second line tells the AWT framework to lay out the container if necessary. These two tasks are seperated for performance reasons. Many things may cause a container to be invalidate. A single call to validate can take care of the container after mutliple calls to invalidate. This is one of the reasons why TableLayout does not automatically redo the layout of a container when setRow is called. The other reason is that a single instance of TableLayout or any layout manager could be used for multiple containers.

Note that you should never call Container.doLayout() to accomplish this task. doLayout should never be called directly by you application. The AWT framework calls this method and the precise effects of this call are subject to change between releases of Java.

Notice that when the row is toggled the size of the list box is adjusted automatically to fill in the remaining space or to give up adaquate space for the toggled row. This happens because the row occupied by the list box has a height of TableLayout.FILL, so it automatically fills the remaining space.

If you want to toggle the visibility of form elements without changing the size or positions or other controls, then you should call the setVisible method of the controls rather than resize rows or columns.

Listing 6: Toggle a Row
package example2; import java.awt.*; import java.awt.event.*; import javax.swing.*; import java.util.*; import info.clearthought.layout.TableLayout; /** * Example of components spanning cells. * * @author Daniel E. Barbalace * @version 1.0, June 14, 2005 */ public class Toggle1 implements ActionListener { /** Instance of TableLayout we are using */ private static TableLayout layout; /** Checkbox that indicates whether or not to notify the user when the Internet connection goes down. */ private static JCheckBox checkboxNotify; /** Container using the layout */ private static Container container; /** * Runs the program. */ public static void main (String args[]) { // Create frame JFrame frame = new JFrame("Local Area Connection - Primary Properties"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Items to display in list control Vector listElement = new Vector(); listElement.add("Client for Microsoft Networks"); listElement.add("File and Print Sharing for Microsoft Networks"); listElement.add("QoS Packet Scheduler"); listElement.add("Microsoft TCP/IP version 6"); listElement.add("Internet Protocol (TCP/IP"); // Create controls JLabel labelConnect = new JLabel("Connect using:"); JTextField textfieldConnect = new JTextField("Intel(R) PRO/100 VE Network Connection"); JButton buttonConfigure = new JButton("Configure..."); JLabel labelUse = new JLabel("This connection uses the following items:"); JList list = new JList(listElement); JScrollPane scrollPane = new JScrollPane(list); JButton buttonInstall = new JButton("Install"); JButton buttonUninstall = new JButton("Uninstall"); JButton buttonProperties = new JButton("Properties"); JCheckBox checkboxShowIcon = new JCheckBox("Show icon in notification area when connected"); checkboxNotify = new JCheckBox("Notify me when this connection has limited or no connectivity"); checkboxNotify.addActionListener(new Toggle1()); JLabel labelTimeout = new JLabel("Timeout:"); JTextField textfieldTimeout = new JTextField(); JButton buttonOK = new JButton("OK"); JButton buttonCancel = new JButton("Cancel"); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double emptySpace = 10; double [] columnSize = {border, 1.0 / 3.0, TableLayout.FILL, 1.0 / 3.0, border}; double [] rowSize = {border, border}; layout = new TableLayout(columnSize, rowSize); layout.setVGap(2); layout.setHGap(5); container = frame.getContentPane(); container.setLayout(layout); // Add controls layout.insertRow(1, p); container.add(labelConnect, "1, 1, 3, 1"); layout.insertRow(2, p); container.add(textfieldConnect, "1, 2, 2, 2"); container.add(buttonConfigure, "3, 2"); layout.insertRow(3, emptySpace); layout.insertRow(4, p); container.add(labelUse, "1, 4, 3, 4"); layout.insertRow(5, TableLayout.FILL); container.add(scrollPane, "1, 5, 3, 5"); layout.insertRow(6, p); container.add(buttonInstall, "1, 6"); container.add(buttonUninstall, "2, 6"); container.add(buttonProperties, "3, 6"); layout.insertRow(7, emptySpace); layout.insertRow(8, p); container.add(checkboxShowIcon, "1, 8, 3, 8"); layout.insertRow(9, p); container.add(checkboxNotify, "1, 9, 3, 9"); layout.insertRow(10, p); container.add(labelTimeout, "1, 10, RIGHT, CENTER"); container.add(textfieldTimeout, "2, 10, 3, 10"); layout.insertRow(11, emptySpace); layout.insertRow(12, p); container.add(buttonOK, "2, 12"); container.add(buttonCancel, "3, 12"); // Note: We don't want to show the timeout row initially because the // checkbox will not be checked. However, we want to include the // timeout row's height in the calculation done by Frame.pack(). // So we call updateDynamicRows() after pack() has been called. // Show frame frame.pack(); updateDynamicRows(); frame.setVisible(true); frame.toFront(); } /** * Invoked when the checkbox is toggled. */ public void actionPerformed (ActionEvent e) { updateDynamicRows(); } /** * Updates the visibility of dynamic rows. */ private static void updateDynamicRows() { layout.setRow(10, checkboxNotify.isSelected() ? TableLayout.PREFERRED : 0); container.invalidate(); container.validate(); } }

Switching between groups of rows

Multiple rows or columns can be toggled just as easily and a single row. One could argue that the timeout control in the previous example should just be disabled rather than effectively removed. However, the benefits of toggle rows and columns becomes more apparent when an application switches between two sets of controls based upon the values specified in radio buttons or drop-down menus.

In this example we toggle several sets of rows based on the item selected in a JComboBox. There is some overlap in the sets of rows. The three possible views of the application are shown below.

screen shot

When an address type is selected all the dyanmic rows are zero'd out in a loop.

        for (int y = rowStart; y <= rowStop + 1; y++)
            layout.setRow(y, 0);

Then a selected range of rows is given non-zero sizes. Rows not containing form elements (every third row) are given a fixed size for spacing. The others are given their preferred size.

        for (int y = rowStart; y <= rowStop + 1; y++)
        {
            double size = ((y - rowStart) % 3 == 2) ? EMPTY_SPACE : TableLayout.PREFERRED;
            layout.setRow(y, size);
        }

Finally, invalidate and validate are called once. Only then does TableLayout spend CPU cycles calculating the layout. The result is an instaneous update of the window with the correct rows showing.

This example also shows how the TableLayout API can be used to create dynamic layouts without hard-coding cell coordinates. Rows are added on the fly and row numbers are calculated at run-time. The TableConstraints class is used to place a row immediately before the OK and Cancel buttons and to center these buttons in the window as shown below.

        TableLayoutConstraints tlc = layout.getConstraints(panelButton);
        tlc.hAlign = TableLayout.CENTER;
        layout.setConstraints(panelButton, tlc);
        
        layout.insertRow(tlc.row1, TableLayout.FILL);

Listing 7: Switching Rows
package example2; import java.awt.*; import java.awt.event.*; import javax.imageio.ImageIO; import javax.swing.*; import java.util.*; import info.clearthought.layout.TableLayout; import info.clearthought.layout.TableLayoutConstraints; /** * Example of components spanning cells. * * @author Daniel E. Barbalace * @version 1.0, June 20, 2005 */ public class Toggle2 implements ActionListener { /** Indicates an internal office address */ private static final String INTERNAL = "Internal"; /** Indicates a domestic (US) address */ private static final String DOMESTIC = "Domestic"; /** Indicates an international (non-US) address */ private static final String INTERNATIONAL = "International"; /** Empty space for asthetics */ private static double EMPTY_SPACE = 10; /** Instance of TableLayout we are using */ private TableLayout layout; /** Container using the layout */ private Container container; /** Identifies the type of address */ private JComboBox comboType; // A whole bunch of controls that this application will use. JLabel labelOffice = new JLabel("Office"); JTextField textOffice = new JTextField(5); JLabel labelBuilding = new JLabel("Building"); JComboBox comboBuilding = new JComboBox(new String [] {"A", "B", "C"}); JLabel labelStreet = new JLabel("Street"); JLabel labelCity = new JLabel("City"); JLabel labelState = new JLabel("State"); JLabel labelZip = new JLabel("Zip Code"); JLabel labelProvidence = new JLabel("Providence"); JLabel labelCountry = new JLabel("Country"); JTextField textStreet = new JTextField(25); JTextField textCity = new JTextField(25); JTextField textState = new JTextField(2); JTextField textZip = new JTextField(5); JTextField textProvidence = new JTextField(25); JTextField textCountry = new JTextField(25); /** * Runs the program. */ public static void main (String args[]) { new Toggle2(); } public Toggle2() { // Create frame JFrame frame = new JFrame("Toggling Groups of Rows"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); // Create and set layout double p = TableLayout.PREFERRED; double f = TableLayout.FILL; double border = 10; double [] columnSize = {border, f, border}; double [] rowSize = {border, border}; layout = new TableLayout(columnSize, rowSize); container = frame.getContentPane(); container.setLayout(layout); // Create controls JLabel labelType = new JLabel("Type of Address"); comboType = new JComboBox(new String [] {INTERNAL, DOMESTIC, INTERNATIONAL}); comboType.setEditable(false); comboType.addActionListener(this); comboBuilding.setEditable(false); JButton buttonOK = new JButton("OK"); JButton buttonCancel = new JButton("Cancel"); JPanel panelButton = new JPanel(); double p2 = buttonCancel.getPreferredSize().width; panelButton.setLayout(new TableLayout(new double [][] {{p2, 5, p2}, {p}})); panelButton.add(buttonOK, "0, 0"); panelButton.add(buttonCancel, "2, 0"); // Add controls addControl(labelType); addControl(comboType); addSpace(); addControl(labelOffice); addControl(textOffice); addSpace(); addControl(labelBuilding); addControl(comboBuilding); addSpace(); addControl(labelStreet); addControl(textStreet); addSpace(); addControl(labelCity); addControl(textCity); addSpace(); addControl(labelState); addControl(textState); addSpace(); addControl(labelZip); addControl(textZip); addSpace(); addControl(labelProvidence); addControl(textProvidence); addSpace(); addControl(labelCountry); addControl(textCountry); addSpace(); // Add the button panel and then center justify it addControl(panelButton); TableLayoutConstraints tlc = layout.getConstraints(panelButton); tlc.hAlign = TableLayout.CENTER; layout.setConstraints(panelButton, tlc); // Add a filler row above the button panel so that the button panel // will stay at the bottom of the window layout.insertRow(tlc.row1, TableLayout.FILL); // Calculate the maximum size the frame would use int numItem = comboType.getItemCount(); Dimension max = new Dimension(0, 0); frame.setVisible(true); for (int i = 0; i < numItem; i++) { comboType.setSelectedIndex(i); updateDynamicRows(); Dimension d = frame.getPreferredSize(); max.width = Math.max(max.width, d.width); max.height = Math.max(max.height, d.height); } comboType.setSelectedIndex(0); updateDynamicRows(); // Show frame frame.setSize(max.width, max.height); frame.toFront(); } /** * Adds a row and a control to the content pane. */ private void addControl (Component component) { int rowNum = layout.getNumRow() - 1; layout.insertRow(rowNum, TableLayout.PREFERRED); container.add(component, new TableLayoutConstraints (1, rowNum, 1, rowNum, TableLayout.LEFT, TableLayout.BOTTOM)); } /** * Adds a blank row so that the window looks nice. */ private void addSpace() { int rowNum = layout.getNumRow() - 1; layout.insertRow(rowNum, EMPTY_SPACE); } /** * Invoked when the checkbox is toggled. */ public void actionPerformed (ActionEvent e) { updateDynamicRows(); } /** * Updates the visibility of dynamic rows. This method first zero outs all * dynamic rows. Then it resizes the relavent rows to * TableLayout.PREFERRED, thereby making their contents visible. Some * rows, like the ones pertaining to the city, are visible for more than * one type of address. */ private void updateDynamicRows() { String type = "" + comboType.getSelectedItem(); int rowMin, rowMax, rowStart = -1, rowStop = -1; rowMin = getRow(labelOffice); rowMax = getRow(textCountry); if (type.equals(INTERNAL)) { rowStart = getRow(labelOffice); rowStop = getRow(comboBuilding); } else if (type.equals(DOMESTIC)) { rowStart = getRow(labelStreet); rowStop = getRow(textZip); } else if (type.equals(INTERNATIONAL)) { rowStart = getRow(labelProvidence); rowStop = getRow(textCountry); } if (rowMin >= 0 && rowMax >= 0) for (int y = rowMin; y <= rowMax; y++) layout.setRow(y, 0); if (rowStart >= 0 && rowStop >= 0) for (int y = rowStart; y <= rowStop + 1; y++) { double size = ((y - rowStart) % 3 == 2) ? EMPTY_SPACE : TableLayout.PREFERRED; layout.setRow(y, size); } if (type.equals(INTERNATIONAL)) { rowStart = getRow(labelStreet); rowStop = getRow(textCity); if (rowStart >= 0 && rowStop >= 0) for (int y = rowStart; y <= rowStop + 1; y++) { double size = ((y - rowStart) % 3 == 2) ? EMPTY_SPACE : TableLayout.PREFERRED; layout.setRow(y, size); } } container.invalidate(); container.validate(); } /** * Gets the row associated with a control. * * @return the row in which a control has been placed or -1 if the component * is not found */ private int getRow (Component component) { TableLayoutConstraints tlc = layout.getConstraints(component); return (tlc == null) ? -1 : tlc.row1; } }

Scrolling

A simple example of a scrolling effect

TableLayout enables the developer to do things that are unimaginable with other layout managers. One such thing is adding scrolling effects to your GUI. In this example we see how a component can be scrolled into existence or bounced from side to side.

A row or column can be scrolled into existence by starting with a zero size and steadily increasing the size until it reaches the desired size. This can be done by using the setRow and setColumn methods. The following code snippet shows a row at the top of a container being continuously scrolled into and out of existence.

        double size = 0;
        double delta = 1;
        
        while (true)
        {
            size += delta;
            layout.setRow(0, size);
            container.invalidate();
            container.validate();
            
            if (size == 0)
                delta = 1;
            else if (size == 50)
                delta = -1;
            
            try {Thread.sleep(DELAY);} catch (InterruptedException e) {}
        }

Side to side scrolling can also be accomplished by adjusting the sizes of empty columns to the immediate left or right of a column containing a component. The next code snippet illustrates this.

        int counter = 0;
        
        while (true)
        {
    	    int offset = BOUNCE_SPACE_2 + (int)
    	        (BOUNCE_SPACE_2 * Math.sin(0.01 * Math.PI * counter++));
    		
            layout.setColumn(0, offset);
            layout.setColumn(2, BOUNCE_SPACE - offset);
            container.invalidate();
            container.validate();
            
            try {Thread.sleep(DELAY);} catch (InterruptedException e) {}
        }

Listing 8: Simple Scrolling Example
package example2; import java.awt.*; import javax.swing.*; import info.clearthought.layout.TableLayout; /** * Example of components spanning cells. * * @author Daniel E. Barbalace * @version 1.0, June 21, 2005 */ public class Scrolling1 implements Runnable { /** Delay between updates in milliseconds */ private static final int DELAY = 1000 / 60; /** Space devoted to left/right bouncing */ private static final int BOUNCE_SPACE = 200; /** Half of the above space */ private static final int BOUNCE_SPACE_2 = BOUNCE_SPACE / 2; /** Instance of TableLayout we are using */ private TableLayout layout; /** Container being laid out */ private Container container; /** Thread used to scroll top button */ private Thread threadTop; /** Thread used to scroll bottom buttom */ private Thread threadBottom; /** * Runs the program. */ public static void main (String args[]) { new Scrolling1(); } /** * Creates the app. */ public Scrolling1() { // Create frame JFrame frame = new JFrame("Example of scrolling components"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setResizable(false); // Create controls JButton buttonTop = new JButton("Scrolling"); JButton buttonBottom = new JButton("By your command"); // Create and set layout double f = TableLayout.FILL; double p = TableLayout.PREFERRED; double [] columnSize = {0, p, BOUNCE_SPACE}; double [] rowSize = {0, f, p}; layout = new TableLayout(columnSize, rowSize); container = frame.getContentPane(); container.setLayout(layout); // Add controls container.add(buttonTop, "0, 0, 2, 0"); container.add(buttonBottom, "1, 2"); // Show frame Dimension d = frame.getPreferredSize(); frame.setSize(d.width, 200); frame.setVisible(true); frame.toFront(); // Start the scrolling effects threadTop = new Thread(this); threadBottom = new Thread(this); threadTop.start(); threadBottom.start(); } /** * Routes threads. */ public void run() { Thread thread = Thread.currentThread(); if (thread == threadTop) runTop(); else if (thread == threadBottom) runBottom(); } /** * Scrolls the top button into and out of existence. */ private void runTop() { double size = 0; double delta = 1; while (true) { size += delta; layout.setRow(0, size); container.invalidate(); container.validate(); if (size == 0) delta = 1; else if (size == 50) delta = -1; try {Thread.sleep(DELAY);} catch (InterruptedException e) {} } } /** * Scrolls the bottom button left and right. */ private void runBottom() { int counter = 0; while (true) { int offset = BOUNCE_SPACE_2 + (int) (BOUNCE_SPACE_2 * Math.sin(0.01 * Math.PI * counter++)); layout.setColumn(0, offset); layout.setColumn(2, BOUNCE_SPACE - offset); container.invalidate(); container.validate(); try {Thread.sleep(DELAY);} catch (InterruptedException e) {} } } }

Making an entrance

In this example we take the web page viewer from previous examples and add some scrolling effects. First we scroll the banner down from the top of the window. Then we scroll the left navigation menu from the left edge of the window. The scrolling effects are done with the following code.

        // Heading
        Dimension d = heading.getPreferredSize();
        for (int i = 0; i < d.height; i++)
        {
            layout.setRow(0, i);
            container.invalidate();
            container.validate();
            sleep();
        }
        layout.setRow(0, TableLayout.PREFERRED);
        
        // Left menu
        d = leftMenu.getPreferredSize();
        for (int i = 0; i < d.width - 1; i += 2)
        {
            layout.setColumn(0, i);
            container.invalidate();
            container.validate();
            heading.repaint();
            sleep();
        }
        layout.setColumn(0, TableLayout.PREFERRED);

After scrolling a row or column, we lock it to its preferred size with lines like layout.setRow(0, i);. So when the window is resized, the layout is what we expect.

There are many more special effects you can add to an application using TableLayout. When toggling between sets of controls instead of instaneously switching from one set to the other, you could shrink away the first set and then grow the second one. The possibilites are limited only by imagination.

Listing 9: Making an Entrance
package example2; import java.awt.*; import java.awt.event.*; import java.io.*; import java.net.*; import java.util.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.tree.*; import info.clearthought.layout.*; /** * BorderLeftMenu extends the BorderStatus demonstration by adding a left * navigation menu. * * @author Daniel E. Barbalace * @version 1.0, Jun 17, 2005 */ public class Scrolling2 implements ActionListener, TreeSelectionListener { /** Main frame */ private JFrame frame; /** Main container */ private Container container; /** Heading */ private Heading heading; /** Used to display the path of the file being displayed */ private JLabel labelFilename; /** When selected, files will be displayed as web pages */ private JRadioButton radioWebPage; /** When selected, files will be displayed as text */ private JRadioButton radioText; /** Used to display HTML files as web pages */ private JEditorPane paneWeb; /** Used to display HTML files as text */ private JTextPane paneText; /** Used to scroll document view */ private JScrollPane scrollPane; /** Open file menu item */ private JMenuItem menuOpenFile; /** Open URL item */ private JMenuItem menuOpenUrl; /** Exit menu item */ private JMenuItem menuExit; /** Status bar */ private StatusBar statusBar; /** Left navigation menu */ private JTree leftMenu; /** Layout used by the frame */ private TableLayout layout; /** * Runs the program. */ public static void main (String args[]) { new Scrolling2(); } /** * Creates the application's GUI. */ public Scrolling2() { // Create frame frame = new JFrame("Border with a heading, status bar, and left menu"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Create menu JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("File"); menuOpenFile = menu.add("Open File"); menuOpenUrl = menu.add("Open URL"); menuExit = menu.add("Exit"); menuOpenFile.addActionListener(this); menuOpenUrl.addActionListener(this); menuExit.addActionListener(this); menuBar.add(menu); frame.setJMenuBar(menuBar); // Create controls heading = new Heading(); JLabel labelFile = new JLabel("File:"); labelFilename = new JLabel(""); JLabel labelView = new JLabel("View as:"); radioWebPage = new JRadioButton("Web Page"); radioText = new JRadioButton("Text"); ButtonGroup groupView = new ButtonGroup(); groupView.add(radioWebPage); groupView.add(radioText); radioWebPage.setSelected(true); radioWebPage.addActionListener(this); radioText.addActionListener(this); paneWeb = new JEditorPane(); paneWeb.setEditable(false); paneText = new JTextPane(); paneText.setEditable(false); scrollPane = new JScrollPane(paneWeb); scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); scrollPane.setPreferredSize(new Dimension(600, 400)); scrollPane.setMinimumSize(new Dimension(10, 10)); statusBar = new StatusBar(); Vector v = new Vector(); v.add("/sample.html"); v.add("/hypercube.html"); v.add("/itox.html"); v.add("/TableLayout.html"); v.add("/feedback.html"); leftMenu = new JTree(v); leftMenu.addTreeSelectionListener(this); Color backgroundColor = new Color(26, 80, 184); DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer(); renderer.setTextNonSelectionColor(Color.WHITE); renderer.setBackground(backgroundColor); renderer.setBackgroundNonSelectionColor(backgroundColor); leftMenu.setCellRenderer(renderer); leftMenu.setBackground(backgroundColor); // Create and set layout double p = TableLayout.PREFERRED; double border = 10; double [] columnSize = {p, border, border, p, p, p, TableLayout.FILL, border}; double [] rowSize = {p, border, p, p, TableLayout.FILL, border, p}; layout = new TableLayout(columnSize, rowSize); layout.setHGap(5); container = frame.getContentPane(); container.setLayout(layout); // Add controls container.add(heading, "0, 0, 7, 0"); container.add(labelFile, "3, 2, RIGHT, BOTTOM"); container.add(labelFilename, "4, 2, 6, 2"); container.add(labelView, "3, 3, RIGHT, BOTTOM"); container.add(radioWebPage, "4, 3"); container.add(radioText, "5, 3"); container.add(scrollPane, "3, 4, 6, 4"); container.add(statusBar, "0, 6, 7, 6"); container.add(leftMenu, "0, 1, 0, 5"); // Show one html file by default setPath("/sample.html"); // Inititialize frame and layout for transitions Component [] c = {labelFile, labelFilename, labelView, radioWebPage, radioText, scrollPane}; for (int i = 0; i < c.length; i++) c[i].setVisible(false); frame.pack(); layout.setRow(0, 0); layout.setColumn(0, 0); // Show frame frame.setVisible(true); frame.toFront(); // Transitions performTransitions(); for (int i = 0; i < c.length; i++) c[i].setVisible(true); } /** * Performs the initial layout transitions. */ private void performTransitions() { // Heading Dimension d = heading.getPreferredSize(); for (int i = 0; i < d.height; i++) { layout.setRow(0, i); container.invalidate(); container.validate(); sleep(); } layout.setRow(0, TableLayout.PREFERRED); // Left menu d = leftMenu.getPreferredSize(); for (int i = 0; i < d.width - 1; i += 2) { layout.setColumn(0, i); container.invalidate(); container.validate(); heading.repaint(); sleep(); } layout.setColumn(0, TableLayout.PREFERRED); } /** * Waits for a standard amount of time. */ private void sleep() { try { Thread.sleep(1); } catch (InterruptedException e) {} } /** * Sets the path of the document to view. * * @param path full or relative path of document */ private void setPath (String path) { // Attempt to open file in jar URL url = Scrolling2.class.getResource(path); // Attempt to open a full URL if (url == null) { try { url = new URL(path); } catch (MalformedURLException e) {} } // Attempt to open a local file if (url == null) { try { File file = new File(path); url = file.toURL(); } catch (MalformedURLException e) {} } // If any of the attempts succeeded, open the url if (url != null) { try { paneWeb.setPage(url); paneText.setText(getContent(url)); labelFilename.setText(path); statusBar.setText("Loaded " + url); } catch (IOException e) { paneWeb.setText(""); paneText.setText(""); labelFilename.setText(""); statusBar.setText("Attempted to read a bad URL: " + url); } } else { statusBar.setText("Couldn't find file: " + path); } } /** * Gets the contents of a URL as a string. * * @param url document to get * * @return a string containing the URL's contents */ private String getContent (URL url) { StringBuffer content = new StringBuffer(); try { String line = ""; BufferedReader input = new BufferedReader(new InputStreamReader(url.openStream())); while (line != null) { content.append(line); if (line.length() > 0) content.append('\n'); line = input.readLine(); } input.close(); } catch (MalformedURLException me) {} catch (IOException e) {} return content.toString(); } /** * Invoked when one of the radio buttons is selected. */ public void actionPerformed (ActionEvent e) { Object source = e.getSource(); if (source == radioWebPage) scrollPane.setViewportView(paneWeb); else if (source == radioText) scrollPane.setViewportView(paneText); else if (source == menuOpenFile) { JFileChooser fc = new JFileChooser(); int returnVal = fc.showOpenDialog(menuOpenFile); if (returnVal == JFileChooser.APPROVE_OPTION) { File file = fc.getSelectedFile(); setPath(file.getPath()); } } else if (source == menuOpenUrl) { String message = "URL to open"; String s = (String) JOptionPane.showInputDialog (frame, message, "Open URL", JOptionPane.PLAIN_MESSAGE, null, null, null); if ((s != null) && (s.length() > 0)) setPath(s); } else if (source == menuExit) System.exit(0); } /** * Invoked when the value of the left menu changes. */ public void valueChanged (TreeSelectionEvent e) { TreePath path = e.getNewLeadSelectionPath(); Object [] array = path.getPath(); String item = (array.length == 2) ? "" + array[1] : ""; if (item.length() > 0) setPath(item); } /** * A simple graphic to display at the top of the GUI, just below the menu * bar. */ private class Heading extends Component { public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); return new Dimension(400, fm.getHeight() * 3); } public void paint (Graphics g) { Dimension d = getSize(); Graphics2D g2 = (Graphics2D) g; GradientPaint gradient = new GradientPaint (leftMenu.getSize().width, 0, new Color(26, 80, 184), d.width, 0, frame.getBackground(), false); g2.setPaint(gradient); g2.fill(new Rectangle(d.width, d.height)); g2.setPaint(Color.white); int x = 0; int i = 0; String [] letter = {"J", "a", "v", "a"}; // Some optimizing done since we will call this method often // during the transitions double coeff = 0.02 * Math.PI; double vMax = 0.25 * d.height; double phaseDelta = coeff * 10; double phase = 0; double [] offset = new double[10]; for (int c = 0; c < 10; c++) { offset[c] = vMax * Math.sin(phase); phase += phaseDelta; } int c = 0; int halfHeight = d.height >> 1; while (x < d.width) { g2.translate(x, offset[c]); g2.drawString(letter[i++], 0, halfHeight); g2.translate(-x, -offset[c++]); x += 10; if (i == letter.length) i = 0; if (c == 10) c = 0; } } } /** * A simple status bar. */ private class StatusBar extends Component { private String text = ""; public StatusBar() { setForeground(Color.BLACK); setBackground(Color.LIGHT_GRAY); } public void setText (String text) { if (text != null) { this.text = text; repaint(); } } public String getText() { return text; } public Dimension getPreferredSize() { FontMetrics fm = getFontMetrics(getFont()); int width = fm.stringWidth(text) + 4; int height = fm.getMaxAscent() + fm.getMaxDescent() + 2; return new Dimension(width, height); } public void paint (Graphics g) { FontMetrics fm = getFontMetrics(getFont()); Dimension d = getSize(); int baseline = d.height - fm.getMaxDescent() - 1; g.setColor(getBackground()); g.fillRect(0, 0, d.width, d.height); g.setColor(getForeground()); g.drawString(text, 2, baseline); g.setColor(Color.black); g.drawRect(0, 0, d.width - 1, d.height - 1); } } }

Finale

Component Orientation

Internationalization of applications has become important, and some languages are read right to left. The AWT framework provides a mechanism for handling such languages. It is called component orientation. (See Component Orientation in Swing for details.) Component orientation affects layouts because components are supposed to be laid out in the same orientation as their container. That means in a left-to-right container the OK button is left of the Cancel button, but in a right-to-left container the OK button is on the right hand side.

TableLayout has built in support for component orientation. This support was added without compromising compatibility with older JDKs that do not have component orientation.

The ComponentOrientation class has three constants: LEFT_TO_RIGHT, RIGHT_TO_LEFT, UNKNOWN. The default value for any container is UNKNOWN, which is equivalent to LEFT_TO_RIGHT so as to preserve the behavior of existing applications. There is no TOP_TO_BOTTOM or BOTTOM_TO_TOP constants. Furthermore, the ComponentOrientation class is final and has a single private constructor to prevent developers from creating new orientations. Nevertheless, TableLayout supports and has been tested with all possible permutations including the ones not currently available.

TableLayout introduces two new justifications to support component orientation: LEADING and TRAILING. The leading justification aligns components along the leading edge of a cell or group of cells. For LTR languages, this is the left edge. For RTL languages, this is the right edge. The trailing justification aligns components along the trailing (opposite) edge. Since the ComponentOrientation class lacks a isTopToBottom method, these two constants can only be applied to the horizontal justification of a component.

It is recommended that any application wishing to support Internationalization used the LEADING and TRAILING constants instead of LEFT and RIGHT. Porting an existing application to use these constants is not difficult, but using the constants from the start of development puts the developer in the right frame of mind.

The application below demonstrates the use of component orientation with TableLayout. The user can change the orientation with the window's menu.

Listing 10: Component Orientation
package example2; import java.awt.*; import java.awt.event.*; import javax.swing.*; import info.clearthought.layout.TableLayout; /** * This example shows how to use component orientation and the corresponding * justifications. * * @author Daniel E. Barbalace * @version 1.0, Jun 27, 2005 */ public class Bidi implements ActionListener { /** Application's frame */ private JFrame frame; /** Menu to select left to right component orientation */ private JMenuItem menuLtr; /** Menu to select right to left component orientation */ private JMenuItem menuRtl; /** Menu to select the unknown component orientation */ private JMenuItem menuUnknown; /** * Runs the program. */ public static void main (String args[]) { new Bidi(); } /** * Creates the application's GUI. */ public Bidi() { // Create frame frame = new JFrame("Component Orientation and Bidirectional Support"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container pane = frame.getContentPane(); // Create menu JMenuBar menuBar = new JMenuBar(); frame.setJMenuBar(menuBar); JMenu menu = new JMenu("Component Orientation"); menuBar.add(menu); menuLtr = new JCheckBoxMenuItem("Left to right"); menuRtl = new JCheckBoxMenuItem("Right to left"); menuUnknown = new JCheckBoxMenuItem("Unknown"); menu.add(menuLtr); menu.add(menuRtl); menu.add(menuUnknown); menuLtr.addActionListener(this); menuRtl.addActionListener(this); menuUnknown.addActionListener(this); menuLtr.setSelected(true); // Create all controls JLabel labelName = new JLabel("Name"); JLabel labelAddress = new JLabel("Address"); JLabel labelCity = new JLabel("City"); JLabel labelState = new JLabel("State"); JLabel labelZip = new JLabel("Zip"); JTextField textfieldName = new JTextField(10); JTextField textfieldAddress = new JTextField(20); JTextField textfieldCity = new JTextField(10); JTextField textfieldState = new JTextField(2); JTextField textfieldZip = new JTextField(5); JButton buttonOk = new JButton("OK"); JButton buttonCancel = new JButton("Cancel"); JPanel panelButton = new JPanel(); panelButton.add(buttonOk); panelButton.add(buttonCancel); // Create and set layout // b - border // f - FILL // p - PREFERRED // vs - vertical space between labels and text fields // vg - vertical gap between form elements // hg - horizontal gap between form elements double b = 10; double f = TableLayout.FILL; double p = TableLayout.PREFERRED; double vs = 5; double vg = 10; double hg = 10; double size[][] = {{b, f, hg, p, hg, p, b}, {b, p, vs, p, vg, p, vs, p, vg, p, vs, p, vg, f, p, b}}; TableLayout layout = new TableLayout(size); pane.setLayout (layout); // Add all controls pane.add(labelName, "1, 1, 5, 1, LEADING, BOTTOM"); pane.add(textfieldName, "1, 3, 5, 3"); pane.add(labelAddress, "1, 5, 5, 5, LEADING, BOTTOM"); pane.add(textfieldAddress, "1, 7, 5, 7"); pane.add(labelCity, "1, 9, LEADING, BOTTOM"); pane.add(textfieldCity, "1, 11"); pane.add(labelState, "3, 9, LEADING, BOTTOM"); pane.add(textfieldState, "3, 11, LEADING, BOTTOM"); pane.add(labelZip, "5, 9, LEADING, BOTTOM"); pane.add(textfieldZip, "5, 11"); pane.add(panelButton, "1, 14, 5, 14"); frame.pack(); frame.setResizable(false); frame.setVisible(true); } /** * Invoked when one of the menu items is selected. */ public void actionPerformed (ActionEvent e) { Object source = e.getSource(); if (source == menuLtr) { frame.applyComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT); menuLtr.setSelected(true); menuRtl.setSelected(false); menuUnknown.setSelected(false); } else if (source == menuRtl) { frame.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT); menuLtr.setSelected(false); menuRtl.setSelected(true); menuUnknown.setSelected(false); } else if (source == menuUnknown) { frame.applyComponentOrientation(ComponentOrientation.UNKNOWN); menuLtr.setSelected(false); menuRtl.setSelected(false); menuUnknown.setSelected(true); } frame.invalidate(); frame.validate(); } }

Grid Class

Sometimes it is useful to see the grid being used by an instance of TableLayout. The following class will allow you to see the grid in any container you wish. Running the main method of this class without any parameters will show the grid and the gaps in the Multicell justification example application. The output is shown below.

screen shot

Listing 11: Seeing the Grid
package example2; import java.awt.*; import java.lang.reflect.*; import javax.swing.*; import info.clearthought.layout.*; /** * This class can show a grid on top of any container that uses TableLayout. * * @author Daniel E. Barbalace * @version 1.0, Jun 14, 2005 */ public final class Grid extends Component { private static Color gridColor = Color.BLACK; private static Color fillColor = null; /** * Runs the program. */ public static void main (String args[]) { setFillColor(Color.BLUE); String className = "example2.MulticellJustify"; if (args.length == 1) className = args[0]; try { Class cls = Class.forName(className); Class [] parameterType = {args.getClass()}; Object [] parameter = {new String[0]}; Method method = cls.getMethod("main", parameterType); method.invoke(null, parameter); Frame [] frame = Frame.getFrames(); if (frame.length == 1) { if (frame[0] instanceof JFrame) showGrid(((JFrame) frame[0]).getContentPane()); else showGrid(frame[0]); } } catch (Exception e) { e.printStackTrace(); } } /** * Shows a grid on a given container provided that the container uses * TableLayout. * * @param container container using TableLayout. May not be null. */ public static void showGrid (Container container) { if (container == null) throw new IllegalArgumentException("Parameter container cannot be null"); LayoutManager l = container.getLayout(); if (!(l instanceof TableLayout)) throw new IllegalArgumentException("Container is not using TableLayout"); TableLayout layout = (TableLayout) l; showGrid(container, layout); container.invalidate(); container.validate(); container.repaint(); } /** * Shows a grid on a given container provided that the container uses * TableLayout. * * @param container container using TableLayout. May not be null. * @param layout TableLayout instance used by container. May not be null. */ private static void showGrid (Container container, TableLayout layout) { int numRow = layout.getNumRow(); int numCol = layout.getNumColumn(); for (int y = 0; y < numRow; y++) for (int x = 0; x < numCol; x++) { Grid g = new Grid(); container.add(g, new TableLayoutConstraints(x, y), 0); } } /** * Gets the grid color. * * @return the grid color */ public static Color getGridColor() { return gridColor; } /** * Sets the grid color. * * @param color new grid color */ public static void setGridColor (Color color) { gridColor = color; } /** * Gets the fill color. * * @return the fill color */ public static Color getFillColor() { return fillColor; } /** * Sets the fill color. * * @param color new fill color */ public static void setFillColor (Color color) { fillColor = color; } /** * Prevent creation of instances outside of this class. */ private Grid() { } /** * Renders the component. * * @param g graphics canvas */ public void paint (Graphics g) { Dimension d = getSize(); if (gridColor != null) { g.setColor(gridColor); g.drawRect(0, 0, d.width - 1, d.height - 1); } if (fillColor != null) { Graphics2D g2d = (Graphics2D) g; g2d.setComposite (AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f)); g2d.setPaint(fillColor); g2d.fill(new Rectangle(1, 1, d.width - 1, d.height - 1)); } } }

Conclusion

I hope you've enjoyed this second installment of the TableLayout Tutorial series. We have covered more advance TableLayout techniques. First we saw how to make components span rows and columns and how to justify those components. Next we examined how to create various borders, status bars, screen headings, and left navigation panels. We saw a powerful technique for simplifying user interfaces by toggle the visibility of groups of controls based on choices made earlier by the user. We added some special effects to our layouts to make them more visually impressive. We used new justifications for component orientation. Finally, we wrote a class to display the grid in any window or container using TableLayout.

TableLayout offers unprecedented power in crafting user interfaces without complications, awkward coding, or nesting containers. A simple yet effective API allows novices to quickly write interfaces and enables veterans to create professional and dynamic interfaces. Layouts that take hours to build with other managers are built in minutes, and layouts that were inconceivable become easy.