HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

If you haven't tried it yet, Swing is Good. For those of us who've had to wrestle with the java.awt to build GUIs, Swing is a much simpler and more powerful alternative. With its "coming soon" status in the com.sun.java.swing classes in JFC 1.0 upgraded to "officially blessed into Java" as javax.swing in JDK 1.2, Java application developers everywhere should be pleased. Although it's only a Java extension and not part of the core library, we should be fine since right now we really don't need GUI support for doorbells, toasters or electric frying pans.

For general Java programming, Swing works pretty well out of the box. Text fields, text areas, buttons, labels, lists and trees cover a large majority of what the average GUI developer needs. However, for harder-core development, Swing still needs some customization. I'm an infrastructural programmer at heart; every time I need to customize something in Java, I try to do it right, once, and add the improvements to my personal Java packages. Every time I do a new project my toolbox expands a little bit more. While working on a recent project I needed to build a facility for automatically positioning windows. Over the years, I've hacked autopositioning several times in C, C++ and Objective C. This time I decided to do a nice, clean Java implementation.

The Autopositioning Problem
The class that most developers use as the base for a window on a screen in Swing is JFrame. Left to themselves, JFrames are initially positioned at (0,0). When they're shown, each new JFrame will stack up, one on top of the other, with all of their origins at (0,0) - obscuring previously created JFrames, as shown in Figure 1a. In any real application, this gets to be difficult for the user: each new window has to be selected and moved by hand. Autopositioning should be done prior to the first appearance of a window, in a way that lets the user identify it easily in relation to its neighbors.

Figure 1
Figure 1:

The JFrame inherits the java.awt.Frame lineage, along with its ancestor java.awt.Component handling positioning. A component is positioned using its setBounds() method, and the new intended position and size of the component are provided as arguments. Assuming we know the size, we need to assign a reasonable new (x,y) location to the JFrame for autopositioning prior to the first call to setBounds().

A popular strategy for keeping windows from obscuring each other is by cascading them - staggering them within an area so that new windows roll across it in an orderly fashion, each slightly offset from the previous one, as shown in Figure 1b. When the position reaches a predetermined limit, it's wrapped around and the staggering cycle starts again. Many windowing applications use this strategy today, and it's the one I constructed, but in a very robust way.

The Cascade Object
The workhorse of our code is the Cascade object. A Cascade implements the cascading behavior in one dimension: an instance steps across locations within an area, wrapping around at the end. The object has two principal behaviors, one to get the current location:

public int location ()

and another to increment it:

public void increment ()

When a Cascade is created it is set with default values that implement a default staggering and wrapping behavior. These values will all have set and get methods so that custom cascading may be configured. Each Cascade requires four parameters to implement a robust behavior: an offset, a position, a step and a wrap, related schematically in Figure 2. The location is equal to the offset+position, and when the Cascade is incremented, step is added to position. Position is then effectively adjusted to be within wrap's limits via a modulus function. The values for wrap must be greater than zero, while the values for offset and step may be assigned any value. Although the initial value of position may be set arbitrarily, subsequent values are calculated using the other parameters.

Figure 2
Figure 2:

Besides these behaviors, there are the requisite canonical inclusions for serialization, cloning and content-based equality tests. It's always a good practice to throw them in because you never know what you may want to do with the object later, and they're easier to add while you're writing the code rather than as an afterthought. In addition to the default constructor, a version that takes the four parameters is also provided. The source code for the Cascade class appears in Listing 1.

It should be noted that after enough cycles of stepping and wrapping, the generated position will begin to repeat. That's the nature of a modulus function applied to a monotonically increasing value. Even though it can't be eliminated, by choosing the step and wrap values carefully, the cycle of repetition can be made quite long - long enough so that only applications that are incrementing the Cascade many, many times will cause it to cycle.

The PointCascade Object
In order to cascade a two-dimensional location, you simply use two Cascade objects, one for the x dimension, and another for the y dimension. The PointCascade class wraps together two Cascade objects under one interface. It allows access to the underlying Cascades directly or manages them indirectly through messages that take java.awt.Point objects as arguments. The same set and get methods that are provided in Cascade are also provided in PointCascade, but with Points as arguments instead of integers. Methods to set and get the xCascade and yCascade are provided as well.

The signature of the increment() method is the same in the PointCascade as it was in the Cascade, but the PointCascade's location method now returns a Point object. This point is cascaded in x and y - we'll use it to position a JFrame. The staggering and wrapping can be set independently for the x and the y coordinates so a large set of noncycled, cascaded points can be created. However, if the Cascade defaults are used, the x and y coordinates will increment in lockstep, and only points on a diagonal will be generated. Again, the way to overcome this is to set up the parameters of the embedded Cascade objects carefully. By adjusting the step and wrap values of either or both of the Cascade objects, the cascading can be made to behave very differently in x and y. The source code for PointCascade is in Listing 2.

The CascadingJFrame Object
To make JFrames cascade themselves automatically, a new location must be retrieved from the PointCascade object when a new JFrame is created, and then the PointCascade must be incremented. Subsequently, when the JFrame is first positioned, its setBounds() method places the JFrame at the new location. The easiest way to do this in Java is through inheritance.

The CascadingJFrame class is a subclass of JFrame that contains a static PointCascade. After the JFrame has been created, the constructor gets the PointCascade's location, increments the PointCascade to set up for the next invocation and sets an autoposition flag to true. The class also overrides both of the JFrame's setBounds() methods. If the autoposition flag is set, the generated location is used in the call to the JFrame's setBounds() method to do the actual positioning. The PointCascade is static since it has to be shared between all the instances of CascadingJFrame. Each CascadingJFrame stores its generated location, but the PointCascade that generates these locations is a common resource. If any of the PointCascade's values are changed after locations have been generated, only the subsequently generated CascadingJFrame locations will be affected. The CascadingJFrame class has static methods to set and get its underlying PointCascade.

The cascading mechanism can also be negated in certain cases if desired. If the application determines that a particular CascadingJFrame is somehow special, then the initial positioning can either be adjusted by calling the CascadingJFrame's setLocation() method with the special location as a Point, turning off the autopositioning altogether, calling the CascadingJFrame's setAutoposition() method with a false value or using an extended constructor.

With a good understanding of the cascading mechanism, complete control of autopositioning is possible. It's as simple as that.

Multiple Cascades
But we can always make it more robust! It isn't enough to simply cascade. I immediately found that I wanted to have different types of windows cascading in different ways. For instance, in one area of the screen I wanted a stack of small windows, and in another a staggered set of larger ones. Applications all have their own idiosyncrasies; my goal was simply to cover most of the regular ones with the simple CascadingJFrame objects. The solution I implemented was to make the CascadingJFrame class contain multiple static PointCascade objects that could be arbitrarily named by the application. When a CascadingJFrame object is constructed, the application can specify the particular PointCascade to use for autopositioning.

To avoid adding complexity for the developer, the simple, unnamed-PointCascade case should continue to work; the calls to CascadingJFrame without a named PointCascade need to remain available. After all, plenty of applications do not require rigorous control or multiple streams of autopositioning. For most developers, using a single PointCascade would suffice, with only a few odd JFrames positioned directly. For these situations a default PointCascade is managed behind the scenes for the unnamed-PointCascade calls.

To implement the multiple PointCascade objects, the CascadingJFrame is made to contain a static hashtable of named PointCascades instead of just a single static PointCascade. The calls to CascadingJFrame without names are connected to the named calls using the default PointCascade's internal name. This way, when an unnamed PointCascade call comes in, the default PointCascade object is retrieved from the hashtable, used to get the location and incremented. If a named call comes in, the particular PointCascade is looked up, used and incremented. If the specified PointCascade is not in the hashtable, a new one is created and used. In this way the application maintains complete control over the autopositioning namespace. The source code for CascadingJFrame is in Listing 3.

Summary
The overall Design of the CascadingJFrame is shown in Figure 3. The important thing to notice is that the CascadingJFrame class contains the hashtable of PointCascade objects, not a CascadingJFrame instance. The instance simply contains the location that was retrieved from a specific PointCascade and an autoposition flag indicating whether or not the retrieved location should be used. The hashtable is a static - effectively shared by all the CascadingJFrame instances.

Figure 3
Figure 3:

Autopositioning frees the developer's head a little, which is good - one less thing to think about. By providing the popular window-cascading behavior, in multiple streams when desired, sophisticated window positioning may be achieved with relatively little effort - by simply setting a few parameters and inheriting from the CascadingJFrame class. It's also interesting to consider extending additional subclasses that offer alternate positioning policies, or perhaps "position-memory" in which the boundaries of particular windows are written to persistent storage whenever they are moved or resized, and recovered when they are reconstituted by the application in a subsequent session.

Finally, a philosophical note: the best infrastructures are invisible and work automatically. The CascadingJFrame class envelops regular initial window positioning, typically to the degree of letting the applications programmer forget about it. And that is the best an infrastructure developer can hope for...making it so easy for others to do their work that they don't have to concern themselves with the underlying framework. If that goal is achieved, the framework was done right.

About the Author
David Anderson is a consulting software engineer at LEXIS-NEXIS, an online information service in Dayton, Ohio. At night he puts on his Java shoes and cranks out reams of code as a contractor. He has been developing software since 1974. Dave can be reached at [email protected] or through the monkey house (http://w3.one.net/~monkey).

	

Listing 1.
 
// Cascade.java  Copyright 1998 David J. Anderson 

import java.io.Serializable; 

/** A Cascade implements a stepable position in a one-dimensional offset 
  wrapping-space. 
  The instance computes a new int location and increments it on demand. 
  The four int parameters which control the instance are 
  <UL> 
  <LI>the <I>offset</I> from the origin, 
  <LI>the current <I>position</I> relative to the offset, 
  <LI>the <I>step</I> which is added to the position during an increment, and 
  <LI>the <I>wrap</I> which bounds the position relative to the offset. 
  </UL> 
**/ 

public class Cascade 
implements Cloneable, Serializable 
{ 
  private int offset; 
  private int position; 
  private int step; 
  private int wrap; 

  /** Creates a new Cascade with default parameters. 
  **/ 
  public Cascade () 
  { 
    this(10,0,25,300); 
  } 

  /** Creates a new Cascade with the given parameters. 
    @param offset the offset from 0 
    @param position the position from the offset 
    @param step the distance the position is incremented 
    @param wrap the basis of the position modulus 
    **/ 
  public Cascade (int offset, int position, int step, int wrap) 
  { 
    setOffset(offset); 
    setPosition(position); 
    setStep(step); 
    setWrap(wrap); 
  } 

  /** Sets the instance's offset. 
    @param offset the offset from 0 
    **/ 
  public void setOffset (int offset) 
  { 
    this.offset = offset; 
  } 

  /** Gets the instance's offset. 
    @return the offset from 0 
    **/ 
  public int getOffset () 
  { 
    return offset; 
  } 

  /** Sets the instance's position. 
    @param position the position from the offset 
    **/ 
  public void setPosition (int position) 
  { 
    this.position = position; 
  } 

  /** Gets the instance's position. 
    @return the position from the offset 
    **/ 
  public int getPosition () 
  { 
    return position; 
  } 

  /** Sets the instance's step. 
    @param step the distance the position is incremented 
    **/ 
  public void setStep (int step) 
  { 
    this.step = step; 
  } 

  /** Gets the instance's step. 
    @return the distance the position is incremented 
    **/ 
  public int getStep () 
  { 
    return step; 
  } 

  /** Sets the instance's wrap. 
    @param wrap the basis of the position modulus 
    **/ 
  public void setWrap (int wrap) 
  { 
    this.wrap = wrap; 
  } 

  /** Gets the instance's wrap. 
    @return the basis of the position modulus 
    **/ 
  public int getWrap () 
  { 
    return wrap; 
  } 

  /** Computes the instance's current location. 
    @return the location 
    **/ 
  public int location () 
  { 
    return offset+position; 
  } 

  /** Increments the instance's location, keeping it within the parameters. 
  **/ 
  public void increment () 
  { 
    position += step; 
    while (position < 0) 
      position += wrap; 
    while (position > wrap) 
      position -= wrap; 
    if (position < 0) 
      position = 0; 
  } 

  /** Compares the contents of the instance with the given Cascade's contents. 
    @return true if the contents of the instance and the given Cascade are 
    the same 
  **/ 
  public boolean equals (Cascade cascade) 
  { 
    return 
      ((cascade != null)&& 
       (getOffset() == cascade.getOffset())&& 
       (getPosition() == cascade.getPosition())&& 
       (getStep() == cascade.getStep())&& 
       (getWrap() == cascade.getWrap())); 
  } 

  /** Clones the instance. 
    @return the cloned instance 
    **/ 
  public Object clone () 
  { 
    Cascade clone = null; 
    try 
      { 
 clone = (Cascade)(super.clone()); 
      } 
    catch (CloneNotSupportedException exception) 
      { 
 throw new InternalError(exception.toString()); 
      } 
    return clone(); 
  } 
} 

Listing 2.
 
// PointCascade.java  Copyright 1998 David J. Anderson 

import java.awt.Point; 
import java.io.Serializable; 

/** A PointCascade implements a stepable position in an two-dimensional offset 
  wrapping-space. 
  The instance computes a new Point location and increments it on demand. 
  The four logical Point parameters which control the instance are 
  <UL> 
  <LI>the <I>offset</I> from the origin, 
  <LI>the current <I>position</I> relative to the offset, 
  <LI>the <I>step</I> which is added to the position during an increment, and 
  <LI>the <I>wrap</I> which bounds the position relative to the offset. 
  </UL> 
  The instance wraps access to two Cascade objects, one for the x-dimension 
  and the other for the y. 
**/ 

public class PointCascade 
implements Cloneable, Serializable 
{ 
  private Cascade xCascade; 
  private Cascade yCascade; 

  /** Creates a new PointCascade with default logical parameters. 
  **/ 
  public PointCascade () 
  { 
    setXCascade(new Cascade(10,0,25,375)); 
    setYCascade(new Cascade(10,0,25,300)); 
  } 

  /** Creates a new PointCascade with the given Cascades. 
    @param xCascade the Cascade for the x dimension 
    @param yCascade the Cascade for the y dimension 
    **/ 
  public PointCascade (Cascade xCascade, Cascade yCascade) 
  { 
    setXCascade(xCascade); 
    setYCascade(yCascade); 
  } 

  /** Creates a new Cascade with the given logical parameters. 
    @param offset the offset from 0,0 
    @param position the position from the offset 
    @param step the distance the position is incremented 
    @param wrap the basis of the position modulus 
    **/ 
  public PointCascade (Point offset, Point position, Point step, Point wrap) 
  { 
    this(); 
    setOffset(offset); 
    setPosition(position); 
    setStep(step); 
    setWrap(wrap); 
  } 

  /** Sets the instance's x dimension Cascade 
    @param xCascade the Cascade for the x dimension 
    **/ 
  public void setXCascade (Cascade xCascade) 
  { 
    this.xCascade = xCascade; 
  } 

  /** Gets the instance's x dimension Cascade 
    @return the Cascade for the x dimension 
    **/ 
  public Cascade getXCascade () 
  { 
    return xCascade; 
  } 

  /** Sets the instance's y dimension Cascade 
    @param xCascade the Cascade for the y dimension 
    **/ 
  public void setYCascade (Cascade yCascade) 
  { 
    this.yCascade = yCascade; 
  } 

  /** Gets the instance's y dimension Cascade 
    @return the Cascade for the y dimension 
    **/ 
  public Cascade getYCascade () 
  { 
    return yCascade; 
  } 

  /** Sets the instance's logical offset 
    @param offset the offset from 0,0 
    **/ 
  public void setOffset (Point offset) 
  { 
    xCascade.setOffset(offset.x); 
    yCascade.setOffset(offset.y); 
  } 

  /** Gets the instance's logical offset 
    @return the offset from 0,0 
    **/ 
  public Point getOffset () 
  { 
    return new Point(xCascade.getOffset(),yCascade.getOffset()); 
  } 

  /** Sets the instance's logical position 
    @param position the position from the offset 
    **/ 
  public void setPosition (Point position) 
  { 
    xCascade.setPosition(position.x); 
    yCascade.setPosition(position.y); 
  } 

  /** Gets the instance's logical position 
    @return the position from the offset 
    **/ 
  public Point getPosition () 
  { 
    return new Point(xCascade.getPosition(),yCascade.getPosition()); 
  } 

  /** Sets the instance's logical step 
    @param step the distance the position is incremented 
    **/ 
  public void setStep (Point step) 
  { 
    xCascade.setStep(step.x); 
    yCascade.setStep(step.y); 
  } 

  /** Gets the instance's logical step 
    @return the distance the position is incremented 
    **/ 
  public Point getStep () 
  { 
    return new Point(xCascade.getStep(),yCascade.getStep()); 
  } 

  /** Sets the instance's logical wrap 
    @param wrap the basis of the position modulus 
    **/ 
  public void setWrap (Point wrap) 
  { 
    xCascade.setWrap(wrap.x); 
    yCascade.setWrap(wrap.y); 
  } 

  /** Gets the instance's logical wrap 
    @return the basis of the position modulus 
    **/ 
  public Point getWrap () 
  { 
    return new Point(xCascade.getWrap(),yCascade.getWrap()); 
  } 

  /** Computes the instance's current location. 
    @return the location 
    **/ 
  public Point location () 
  { 
    return new Point(xCascade.location(),yCascade.location()); 
  } 

  /** Increments the instance's location, keeping it within the parameters. 
  **/ 
  public void increment () 
  { 
    xCascade.increment(); 
    yCascade.increment(); 
  } 

  /** Compares the contents of the instance with the given PointCascade's 
    contents. 
    @return true if the contents of the instance and the given pointCascade 
    are the same 
  **/ 
  public boolean equals (PointCascade pointCascade) 
  { 
    return 
      ((pointCascade != null) && 
       (getOffset().equals(pointCascade.getOffset())) && 
       (getPosition().equals(pointCascade.getPosition())) && 
       (getStep().equals(pointCascade.getStep())) && 
       (getWrap().equals(pointCascade.getWrap()))); 
  } 

  /** Clones the instance. 
    @return the cloned instance 
    **/ 
  public Object clone () 
  { 
    PointCascade clone = null; 
    try 
      { 
 clone = (PointCascade)(super.clone()); 
      } 
    catch (CloneNotSupportedException exception) 
      { 
 throw new InternalError(exception.toString()); 
      } 
    clone.xCascade = (Cascade)(xCascade.clone()); 
    clone.yCascade = (Cascade)(yCascade.clone()); 
    return clone(); 
  } 
} 

Listing 3.
 
// CascadingJFrame.java  Copyright 1998 David J. Anderson 

import com.sun.java.swing.JFrame; 
import java.awt.Point; 
import java.awt.Rectangle; 
import java.io.Serializable; 
import java.util.Hashtable; 

/** A CascadingJFrame implements an initially autopositioning JFrame using a 
  named PointCascade to assign the instance's location. 
  A Hashtable of PointCascades, keyed by name, is maintained in the class. 
  It is accessed when the CascadingJFrame is constructed to get the initial 
  location. 
  Afterwards, on the first call to setBounds, the JFrame's position is 
  adjusted to the cascaded location. 
  The autopositioning may be turned off or adjusted on an instance prior to 
  the setBounds call. 
  **/ 

public class CascadingJFrame 
extends JFrame 
implements Serializable 
{ 
  private static Hashtable pointCascadeHashtable = new Hashtable (); 
  /** The name of the default PointCascade **/ 
  public static final String defaultCascadeName = "_default_"; 

  private Point location; 
  private boolean autoposition; 

  /** Creates a new CascadingJFrame with the given title using the default 
    PointCascade. 
    @param title the text to be placed on the title bar 
  **/ 
  public CascadingJFrame (String title) 
  { 
    this(title,defaultCascadeName,true); 
  } 

  /** Creates a new CascadingJFrame with the given title using the named 
    PointCascade. 
    @param title the text to be placed on the title bar 
    @param cascadeName the name of the PointCascade to retrieve the location 
    from 
    **/ 
  public CascadingJFrame (String title, String cascadeName) 
  { 
    this(title,cascadeName,true); 
  } 

  /** Creates a new CascadingJFrame with the given title using the named 
    PointCascade. 
    @param title the text to be placed on the title bar 
    @param cascadeName the name of the PointCascade to retrieve the location 
    from 
    @param autoposition if false, then no autopositioning will be done for the 
    instance 
    **/ 
  public CascadingJFrame 
  (String title, String cascadeName, boolean autoposition) 
  { 
    super(title); 
    if (autoposition) 
      setLocation(cascadeName); 
    setAutoposition(autoposition); 
  } 

  /** Sets the default PointCascade to the given PointCascade. 
    @param pointCascade the new default PointCascade 
    **/ 

  public static void setPointCascade (PointCascade pointCascade) 
  { 
    setPointCascade(defaultCascadeName,pointCascade); 
  } 

  /** Sets the named PointCascade to the given PointCascade. 
    @param cascadeName the name of the PointCascade 
    @param pointCascade the new PointCascade 
    **/ 
  public static void setPointCascade 
  (String cascadeName, PointCascade pointCascade) 
  { 
    pointCascadeHashtable.put(cascadeName,pointCascade); 
  } 

  /** Gets the default PointCascade. 
    @return the default PointCascade 
    **/ 
  public static PointCascade getPointCascade () 
  { 
    return CascadingJFrame.getPointCascade(defaultCascadeName); 
  } 

  /** Gets the named PointCascade. 
    If a point cascade by that name does not exist, a new one is created. 
    @param cascadeName the name of the PointCascade 
    @return the named PointCascade 
    **/ 
  public static PointCascade getPointCascade (String cascadeName) 
  { 
    if (cascadeName == null) 
      cascadeName = defaultCascadeName; 
    PointCascade pointCascade = 
      (PointCascade)(pointCascadeHashtable.get(cascadeName)); 
    if (pointCascade == null) 
      { 
 pointCascade = new PointCascade(); 
 CascadingJFrame.setPointCascade(cascadeName,pointCascade); 
      } 
    return pointCascade; 
  } 

  /** Sets whether the instance will be autopositioned. 
    @param autoposition true if autopositioning is desired 
    **/ 
  public void setAutoposition (boolean autoposition) 
  { 
    this.autoposition = autoposition; 
  } 

  /** Gets whether the instance will be autopositioned. 
    @return true if autopositioning will occur 
    **/ 
  public boolean getAutoposition () 
  { 
    return autoposition; 
  } 

  /** Sets the instance's location by retrieving it from the named 
    PointCascade. 
    @param cascadeName the name of the PointCascade 
    **/ 
  public void setLocation (String cascadeName) 
  { 
    PointCascade pointCascade = getPointCascade(cascadeName); 
    setLocation(pointCascade.location()); 
    pointCascade.increment(); 
  } 

  /** Sets the instance's location to the specified Point. 
    @param location the new location 
    **/ 
  public void setLocation (Point location) 
  { 
    this.location = location; 
  } 

  /** Gets the instance's location. 
    @return the location 
    **/ 
  public Point getLocation () 
  { 
    return location; 
  } 

  /** Sets the instance's bounds, autopositioning if intended. 
    @param x the new x coordinate of the upper left position 
    @param y the new y coordinate of the upper left position 
    @param width the new width of the instance 
    @param height the new height of the instance 
    **/ 
  public void setBounds (int x, int y, int width, int height) 
  { 
    if (getAutoposition() == true) 
      { 
 Point frameLocation = getLocation(); 
 x = frameLocation.x; 
 y = frameLocation.y; 
 setAutoposition(false); 
      } 
    super.setBounds(x,y,width,height); 
  } 

  /** Sets the instance's bounds, autopositioning if intended. 
    @param r the new bounds of the instance 
    **/ 
  public void setBounds (Rectangle r) 
  { 
    setBounds(r.x,r.y,r.width,r.height); 
  } 
} 
  



 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.