Implementing a Position Layout Manager in Java
by Scott M. Consolatti, IBM
Abstract
This article provides source code for working around a common problem in Java: the lack
of a position layout manager for setting fixed X and Y coordinates and fixed widths and
heights.Java provides layout managers for such properties as border, flow, and grid bag,
so that the size and position of components in a container will update themselves
automatically as the size and position of the container changes. A flow layout is
analogous to an HTML Web page's default layout, in which, if you resize your Web browser,
the components of top to bottom. However, Java does not provide a position layout for
setting a particular X and Y coordinate and setting a particular width and height for
the component. Many Java programmers simply set their container's layout manager to null,
add their components to the container, and then set their X and Y coordinates manually.
Sun, however, warns that this method is not officially supported and that the results on
various platforms and implementations of the Java Virtual Machine (JVM) are not guaranteed
to be consistent. The better solution is to write your own position layout manager, and
this article teaches you how to do so. The entire source code of the solution is provided
in this article so that you may begin using it immediately in your Java programs.
The Primary Interface
Java provides for the writing of your own layout managers via the LayoutManager
interface in the java.awt package. In addition, version 1.1 of the Java Development
Kit (JDK) introduced a new LayoutManager2 interface, with the goal of having all
layout managers consistently use constraint objects to define the constraints, or
size and position information, of each component in a container. As the documentation
says, however, "It does not yet provide full, general support for custom constraint-based
layout managers." Still lacking is a consistent way of setting these constraint objects
into the layout manager and then later getting the constraint object for a particular
component back. To avoid these problems, I prefer using my own LayoutManager3 (shown below),
which adds abstract setConstraints and getConstraints methods for working around this
shortcoming until Java provides full support.
Choosing Your Constraints
Before you can make your layout manager work, you should start by defining the
constraints that will be assigned to each component. As shown below in my
PositionConstraints class, constraints are simple model objects that should
be cloneable. I want each component to be at a particular size and position,
and I also want some basic anchoring and filling capabilities, similar to grid
bag layout, resulting in the following constraint information:
-
A component's position, or X and Y coordinates, held in the X and Y instance
variables of PositionConstraints.
-
A component's size, held in the width and height instance variables, with the ability
to specify -1 to use the component's preferred width or height.
-
Whether or not a component should be anchored at a particular compass point in a container,
such as north, east, or southwest. This information is held in the anchor instance variable.
Public static constants are predefined for each compass point, such as NORTH, EAST, etc.
-
Whether or not a component should fill the width or height of a container. This information
is held in the fill instance variable. Public static constants are predefined for filling
HORIZONTAL, VERTICAL, or BOTH.
-
Whether or not a component should be inset a certain number of pixels from the edge of a
container. This information is held in the insets instance variable.
Telling Your Layout Manager What to Do With Your Constraints
Once you have your constraints, you can create your layout manager to work with
those constraints. As in my PositionLayout class (shown below), this involves
implementing the LayoutManager interface and filling in the required methods that
the interface defines as abstract, such as layoutContainer, minimumLayoutSize, and
preferredLayoutSize.
minimumLayoutSize and preferredLayoutSize both call one common method, layoutSize,
which loops through all of the components in the parent container, looking up their
constraints and keeping a total of the width and height required by the container to
display all of the components. If a constraint has a -1 for width or height, the
component's minimum or preferred width and height are used. Anchor and fill constraints
and insets of both the parent container and component constraints are also factored
into the calculations.
While minimumLayoutSize and preferredLayoutSize define the overall size of the parent
container when its parent container is validated, layoutContainer actually sizes and
positions the components within the available space. This is achieved, again, by looping
through all of the components in the parent container, looking up their constraints, and,
this time, calling setBounds on each component to set its size and position. Again, if a
constraint has a -1 for width or height, preferred size is used, and anchor, fill, and insets
information is accounted for.
We must also fill in the required methods for LayoutManager3: getConstraints and setConstraints.
As with grid bag layout, I chose to hold constraint objects in a private hashtable, named
consTable in this example. setConstraints puts a key:value pair in the hashtable, with the
component being the key and the constraints being the value. If no valid constraints are
specified, default constraints are used, and care is taken to ensure that the constraints
that go into the hashtable are always uniquely cloned. getConstraints uses the common
lookupConstraints method that is also used by layoutSize and layoutContainer, which simply
gets the constraints back out of the hashtable and returns them. Again, if no constraints exist
for a component, default constraints are used. It is hoped that, as Sun provides general support
for custom constraint-based layout managers in the future, it will encapsulate this hashtable
mechanism so that you will not have to manage it yourself in each layout manager.
Just as with the constraints for the components contained within, my position layout manager
itself also has a width and a height. Again, if -1 is defined for width or height, it means
that the parent container will be elastic in size and will grow and shrink to the preferred
size of all of the contained components. If an explicit width or height is set, the parent
container will always be a fixed size, regardless of any contained components and their size
and location. setConstraints defines special code to handle the case of your fixing or making
preferred the size of a component that happens to be a container that itself has a position
layout manager. In that case, I make sure to fix or make preferred the size of the nested
layout manager so that it matches that of its corresponding container.
Using Your New Position Layout Manager
Your position layout manager is now complete and ready for use. My sample PositionExample
class at the end of this article shows an example of some of the ways you can size and
position components with the position layout manager. With your position layout manager,
you'll be ready to arrange components at traditional X and Y coordinates, and you will
be assured of consistent results across various platforms and implementations of the JVM.
LayoutManager3.java
//--------------------------------------------------------------------
// A simple alternative to LayoutManager2 that provides for a
// consistent way of getting and setting constraints for the
// components in a container.
//--------------------------------------------------------------------
import java.awt.*;
import java.io.*;
public interface LayoutManager3 extends LayoutManager, Cloneable
{
public abstract Object getConstraints(Component comp);
public abstract void setConstraints(Component comp, Object cons);
public abstract Object clone();
}
PositionConstraints.java
//--------------------------------------------------------------------
// The constraints for each component in a position layout manager.
// If the width or height is set to -1 (the default), then the
// component will automatically size to its preferred size in that
// dimension.
//--------------------------------------------------------------------
import java.awt.*;
public class PositionConstraints implements Cloneable
{
public static final int NONE = 0;
public static final int BOTH = 1;
public static final int HORIZONTAL = 2;
public static final int VERTICAL = 3;
public static final int NORTH = 1;
public static final int NORTHEAST = 2;
public static final int EAST = 3;
public static final int SOUTHEAST = 4;
public static final int SOUTH = 5;
public static final int SOUTHWEST = 6;
public static final int WEST = 7;
public static final int NORTHWEST = 8;
public int x;
public int y;
public int width;
public int height;
public int anchor;
public int fill;
public Insets insets;
//-1 for width or height means use the preferred size
public PositionConstraints()
{
x = 0;
y = 0;
width = -1;
height = -1;
anchor = NONE;
fill = NONE;
insets = new Insets(0, 0, 0, 0);
}
public Object clone()
{
PositionConstraints p = new PositionConstraints();
p.x = x;
p.y = y;
p.width = width;
p.height = height;
p.anchor = anchor;
p.fill = fill;
p.insets = (Insets)insets.clone();
return p;
}
}
PositionLayout.java
//--------------------------------------------------------------------
// A custom constraint-based layout manager for laying out components
// in the traditional positional way (a component at any X and Y
// coordinate with any width and height).
//--------------------------------------------------------------------
import java.awt.*;
import java.beans.*;
import java.util.*;
public class PositionLayout implements LayoutManager3
{
private static final int PREFERRED = 0;
private static final int MINIMUM = 1;
private Hashtable consTable = new Hashtable();
private PositionConstraints
defaultConstraints = new PositionConstraints();
public int width;
public int height;
//-1 for width or height means use the preferred size
public PositionLayout()
{
width = -1;
height = -1;
}
public PositionLayout(int w, int h)
{
width = w;
height = h;
}
public void addLayoutComponent(String name, Component comp)
{
//Not used. The user should call setConstraints, and then just
//use the generic add(comp) method, like with GridBagLayout.
}
public Object getConstraints(Component comp)
{
return lookupConstraints(comp).clone();
}
public void layoutContainer(Container parent)
{
Component comp;
Component[] comps;
PositionConstraints cons;
Dimension d;
Dimension pD;
int x;
int y;
comps = parent.getComponents();
Insets insets = parent.getInsets();
pD = parent.getSize();
for (int i = 0; i < comps.length; i++)
{
comp = comps[i];
cons = lookupConstraints(comp);
x = cons.x + cons.insets.left + insets.left;
y = cons.y + cons.insets.top + insets.top;
d = comp.getPreferredSize();
if (cons.width != -1)
d.width = cons.width;
if (cons.height != -1)
d.height = cons.height;
if ((cons.fill == PositionConstraints.BOTH) ||
(cons.fill == PositionConstraints.HORIZONTAL))
{
x = insets.left + cons.insets.left;
d.width = pD.width - cons.insets.left - cons.insets.right -
insets.left - insets.right;
}
if ((cons.fill == PositionConstraints.BOTH) ||
(cons.fill == PositionConstraints.VERTICAL))
{
y = insets.top + cons.insets.top;
d.height = pD.height - cons.insets.top - cons.insets.bottom -
insets.top - insets.bottom;
}
switch (cons.anchor)
{
case PositionConstraints.NORTH:
x = (pD.width - d.width) / 2;
y = cons.insets.top + insets.top;
break;
case PositionConstraints.NORTHEAST:
x = pD.width - d.width - cons.insets.right - insets.right;
y = cons.insets.top + insets.top;
break;
case PositionConstraints.EAST:
x = pD.width - d.width - cons.insets.right - insets.right;
y = (pD.height - d.height) / 2;
break;
case PositionConstraints.SOUTHEAST:
x = pD.width - d.width - cons.insets.right - insets.right;
y = pD.height - d.height - cons.insets.bottom -
insets.bottom;
break;
case PositionConstraints.SOUTH:
x = (pD.width - d.width) / 2;
y = pD.height - d.height - cons.insets.bottom -
insets.bottom;
break;
case PositionConstraints.SOUTHWEST:
x = cons.insets.left + insets.left;
y = pD.height - d.height - cons.insets.bottom -
insets.bottom;
break;
case PositionConstraints.WEST:
x = cons.insets.left + insets.left;
y = (pD.height - d.height) / 2;
break;
case PositionConstraints.NORTHWEST:
x = cons.insets.left + insets.left;
y = cons.insets.top + insets.top;
break;
default:
break;
}
comp.setBounds(x, y, d.width, d.height);
}
}
private Dimension layoutSize(Container parent, int type)
{
int newWidth;
int newHeight;
if ((width == -1) || (height == -1))
{
Component comp;
Component[] comps;
PositionConstraints cons;
Dimension d;
int x;
int y;
newWidth = newHeight = 0;
comps = parent.getComponents();
for (int i = 0; i < comps.length; i++)
{
comp = comps[i];
cons = lookupConstraints(comp);
if (type == PREFERRED)
d = comp.getPreferredSize();
else
d = comp.getMinimumSize();
if (cons.width != -1)
d.width = cons.width;
if (cons.height != -1)
d.height = cons.height;
if (cons.anchor == PositionConstraints.NONE)
{
x = cons.x;
y = cons.y;
}
else
{
x = cons.insets.left;
y = cons.insets.top;
}
if ((cons.fill != PositionConstraints.BOTH) &&
(cons.fill != PositionConstraints.HORIZONTAL))
newWidth = Math.max(newWidth, x + d.width);
else
newWidth = Math.max(newWidth, d.width + cons.insets.left +
cons.insets.right);
if ((cons.fill != PositionConstraints.BOTH) &&
(cons.fill != PositionConstraints.VERTICAL))
newHeight = Math.max(newHeight, y + d.height);
else
newHeight = Math.max(newHeight, d.height + cons.insets.top +
cons.insets.bottom);
}
if (width != -1)
newWidth = width;
if (height != -1)
newHeight = height;
}
else
{
newWidth = width;
newHeight = height;
}
Insets insets = parent.getInsets();
return (new Dimension(newWidth + insets.left + insets.right,
newHeight + insets.top + insets.bottom));
}
private PositionConstraints lookupConstraints(Component comp)
{
PositionConstraints p = (PositionConstraints)consTable.get(comp);
if (p == null)
{
setConstraints(comp, defaultConstraints);
p = defaultConstraints;
}
return p;
}
public Dimension minimumLayoutSize(Container parent)
{
return layoutSize(parent, MINIMUM);
}
public Dimension preferredLayoutSize(Container parent)
{
return layoutSize(parent, PREFERRED);
}
public void removeLayoutComponent(Component comp)
{
consTable.remove(comp);
}
public void setConstraints(Component comp, Object cons)
{
if ((cons == null) || (cons instanceof PositionConstraints))
{
PositionConstraints pCons;
if (cons == null)
pCons = (PositionConstraints)defaultConstraints.clone();
else
pCons = (PositionConstraints)
((PositionConstraints)cons).clone();
consTable.put(comp, pCons);
//The following is necessary for nested position layout
//managers. When the constraints of the component are set
//to be elastic or non-elastic, then check to see if the
//component itself is a container with a position layout
//manager and, if so, set the layout manager itself to be
//elastic or non-elastic as necessary.
if (Beans.isInstanceOf(comp, Container.class))
if (((Container)Beans.getInstanceOf(comp,
Container.class)).getLayout()
instanceof PositionLayout)
{
PositionLayout layout;
layout = (PositionLayout)
((Container)Beans.getInstanceOf(comp,
Container.class)).getLayout();
layout.width = pCons.width;
layout.height = pCons.height;
}
}
}
public Object clone()
{
PositionLayout p = new PositionLayout(width, height);
return p;
}
}
PositionExample.java
//--------------------------------------------------------------------
// An example showing how to create a container with a position
// layout and how to add children components to it at various positions.
//--------------------------------------------------------------------
import java.awt.*;
public class PositionExample extends Frame
{
PositionExample()
{
super();
setSize(400, 300);
// Create and set a position layout
PositionLayout posLayout = new PositionLayout();
setLayout(posLayout);
// Create children with various position constraints
PositionConstraints pCons;
pCons = new PositionConstraints();
pCons.x = 300;
pCons.y = 210;
Button preferredSizeButton = new Button("x=300, y=210");
posLayout.setConstraints(preferredSizeButton, pCons);
add(preferredSizeButton);
pCons = new PositionConstraints();
pCons.x = 75;
pCons.y = 50;
pCons.width = 200;
pCons.height = 60;
Button fixedSizeButton = new Button("x=75, y=50, w=200, h=60");
posLayout.setConstraints(fixedSizeButton, pCons);
add(fixedSizeButton);
pCons = new PositionConstraints();
pCons.anchor = PositionConstraints.SOUTHWEST;
Button anchorButton = new Button("Anchor southwest");
posLayout.setConstraints(anchorButton, pCons);
add(anchorButton);
pCons = new PositionConstraints();
pCons.anchor = PositionConstraints.NORTH;
pCons.fill = PositionConstraints.HORIZONTAL;
pCons.insets = new Insets(8, 8, 8, 8);
Button insetButton =
new Button("Anchor north, fill horizontal, inset 8");
posLayout.setConstraints(insetButton, pCons);
add(insetButton);
pCons = new PositionConstraints();
pCons.fill = PositionConstraints.BOTH;
Button fillButton = new Button("Fill both");
posLayout.setConstraints(fillButton, pCons);
add(fillButton);
}
public static void main(String args[])
{
(new PositionExample()).setVisible(true);
}
}
|