Long ago, when Xerox defined the leading edge of the desktop GUI, applications flooded the desktop with top-level windows: main windows, alternative views, palettes, inspectors, dialog boxes spewing dialog boxes, editors, and on and on. A whole multitude of overlapping, titled, monochrome rectangles arranged in a way that mimicked a very messy, real-world desktop. Over time, our desire to simulate a messy pile of papers has waned. Modern applications tend to limit the use of top-level windows to alert, configuration, and other ephemeral tasks that seem to warrant a brief slice of the user's undivided attention. Everything else is packed into a tiled main window that's managed by the application and can be reconfigured by the user. GUI frameworks for building reconfigurable tiled main windows are usually called "docking frameworks." Docking frameworks make it possible to mimic a preternaturally neat desktop. Among the many organizational features that most docking frameworks provide is support for interactively resizing tiles by mouse-dragging in the gaps that separate the tiles.
MultiSplitPane is not a general purpose docking framework. It's a Swing container that just supports a resizable tiled layout of arbitrary components. It's intended to be a generalization of the existing Swing JSplitPane component, which only supports a pair of tiles. The MultiSplitLayout layout manager recursively arranges its components in row and column groups called "splits." Elements of the layout are separated by gaps called "dividers" that can be moved by the user, in the same way as JSplitPane. The overall layout is defined with a simple tree-structured model that can be stored and retrieved to make the user's layout configuration persistent. The initial layout, before the user has intervened, is defined conventionally, in terms of the layout model and the component's preferred sizes.
MultiSplitPane differs from components with similar capabilities in that complex dynamic layouts can be defined without nesting or composition. All of the children managed by a MultiSplitPane are arranged in their rows and columns (and rows within columns and columns within rows) end up separated by divider gaps, but not by extra layout-managing containers. MultiSplitPane's layout class, MultiSplitLayout, is also a little unusual in that it exposes a model of the complete layout. Most layout managers have a complex internal model that represents the layout, and some, like GridBagLayout, even support ad-hoc access to the model. MultiSplitPane provides explicit access to the complete layout model, in the same way that Swing components provide access to their data models. The motivation for this wasn't just flexibility, or separation of concerns. A single explicit layout model means that a more elaborate layout management system, like a docking framework, can be layered on top of MultiSplitPane without requiring burdensome assumptions about the type or structure of the component hierarchy. Having a separable model also means that the layout can be archived and restored by writing and reading (just) the model. The final section in this article, "Making MultiSplitPane Layouts Persistent," describes how to do this, using the java.beans (XMLEncoder and XMLDecoder) persistence API.
Basic MultiSplitPane Usage
Using MultiSplitPane requires two steps. First, a tree model that specifies the layout is created using the MultiSplitLayout's Split, Divider, and Leaf classes. These classes are static inner classes of MultiSplitLayout, so they have names like MultiSplitLayout.Divider (design note: this seemed preferable to MultiSplitLayoutDividerNode; by importing the static inner classes, we can refer to them by the unqualified names, like Divider and Split). Leaf nodes represent components, dividers represent the gaps between components that the user can drag around, and splits represent rows or columns. Components are added to the MultiSplitPane with a constraint that names the Leaf (leaf nodes have a name property) that will specify their bounds.
Here's an example that creates the MultiSplitPane equivalent of JSplitPane. There are just two components, arranged in a row, with a Divider in between.
List children =
Arrays.asList(new Leaf("left"),
new Divider(),
new Leaf("right"));
Split modelRoot = new Split();
modelRoot.setChildren(children);
MultiSplitPane multiSplitPane = new MultiSplitPane();
multiSplitPane.getMultiSplitLayout().setModel(modelRoot);
multiSplitPane.add(new JButton("Left Component"), "left");
multiSplitPane.add(new JButton("Right Component"), "right");
The first block of code creates the model: a Split node with three children. The two Leaf children are named "left" and "right". When we add two JButtons to the MultiSplitPane, we specify the name of the Leaf node that defines their part of the layout with the second constraintContainer.add() argument. The Divider node in the layout, which appears in between the "left" and "right"Leaf nodes just serves as a placeholder for the vertical gap that will be allocated in between the Leaf nodes. The model is shown in Figure 1.
Figure 1. Model for Example 1
To give the example launcher a try, just click the Launch button.
If you run the example, you'll see that the initial layout of the two buttons respects their preferred sizes, shown in Figure 2. You'll also note that you can change the relative widths of the buttons by dragging in the gap, as you'd expect. If you resize the window, making it wider, all of the extra space is allocated to the right button, as seen in Figure 3. This is because, by default, MultiSplitPane allocates extra space to the last component in a row or a column.
Figure 2. Example 1 initial layout
Figure 3. Example 1 after horizontal (window) resize
You can change the way extra space is allocated by setting the weight property of the left and right Leaf nodes. Weights are used to compute what percentage of the extra space, or space reduction, should be allocated to each sibling in a Split. The total weight for a set of siblings should be 1.0. To allocate space equally in the previous example, we'd give each Leaf a weight of 0.5:
Leaf left = new Leaf("left");
Leaf right = new Leaf("right");
left.setWeight(0.5);
right.setWeight(0.5);
List children = Arrays.asList(left, new Divider(), right);
MultiSplitLayout.Split modelRoot = new Split();
modelRoot.setChildren(children);
Figure 4 shows the model for this arrangement.
Figure 4. Model for Example 2: 0.5/0.5 weighted layout
If you try launching this example you'll see that horizontal space is allocated, or deallocated equally, as shown in Figure 5. If you don't move the divider, then the left and right components will not shrink below their minimum widths (which is the same as preferred width for JButtons).
Figure 5. Example 2: 0.5/0.5 weighted layout
It's also probably obvious at this point that defining layout models using the APIs for Split, Leaf, and Divider is a bit tedious for examples. The MultiSplitLayout class provides a parser for a simple syntax that makes it easier to define layout models for examples or test cases. It's not intended as an archive format (see the "Making MultiSplitPane Layouts Persistent" section for a discussion of how to load and store MultiSplitLayout models using XML). The syntax uses parentheses for structure and doesn't require one to specify Dividers; they're added automatically. Here's how the previous example would be coded using this syntax:
The MultiSplitLayout.Node class is just the superclass for Split, Divider, and Leaf. The parseModel() method can be used to generate a single Leaf or a Split. Note also that the syntax for Leaf nodes can be shortened, if the Leaf doesn't specify a weight, to just the Leaf node name. So the first example could be written like this: (ROW left right).
What makes MultiSplitPane interesting is that it can support more complex layouts, where rows contain columns that contain rows, and so on. Here's a more complex example, just to show what's possible.
Figure 6. Example 3: model of a more complicated layout
Run this example to get the layout shown in Figure 7.
Figure 7. Example 3: screenshot
If you drag the first vertical Divider in the previous example, you'll see that it's possible to change the size of the left Leaf node to the left of the Divider and the middle column Split node to the right. By default, it's possible to move the divider to the point where either sibling's size is zero. Once you've moved the Divider, the layout algorithm attempts to leave it where it you put it, despite changes to the MultiSplitPane container's bounds (e.g., as a result of resizing the window). The algorithm used by the MultiSplitLayout layout manager to allocate and deallocate space is described in the next section.
The MultiSplitLayout Algorithm
A MultiSplitLayout is defined by a tree model with Split, Divider, and Leaf nodes as described in the previous section. The layout model recursively subdivides the container's bounded Rectangle into sequences of rectangles, one per node, arranged in rows or columns. Node rectangles are separated by a fixed dividerSize gap. The layout algorithm is applied to the layout's tree model in two passes. The second pass finalizes the bounds of each node and then sets the bounds of components that correspond to Leaf nodes.
The initial MultiSplitLayout is defined by the preferred sizes of the MultiSplitPane's children. At this point, the Dividers are considered to be "floating;" i.e., their positions are defined by the preferred sizes of the nodes that flank them. The MultiSplitLayout node types contribute to the overall layout like this:
Split: If the Split is a row (Split.isRowLayout() is true)
The preferred width of the Split will be the sum of the preferred widths of its (node) children, plus the widths of the dividers. The preferred height of the Split will be the maximum height of its children. The Split's (node) children will always be laid out left to right and will all have the same height. Additional space, or space reduction, is allocated based on the children's weights (0.0 to 1.0). If no weight is specified, the last node is treated as if its weight was 1.0.
Split: If the Split is a column (Split.isRowLayout() is false)
The preferred height of a column Split will be the sum of the preferred heights of its (node) children, plus the heights of the dividers. The preferred width of the column Split will be the maximum width of its children. The Split's (node) children will always be laid out top to bottom and will all have the same width. Additional space, or space reduction, is allocated based on the children's weights (0.0 to 1.0). If no weight is specified, the last node is treated as if its weight was 1.0. Note: this case is logically the same as a Split row.
Leaf
The preferred size of a Leaf is just the preferred size of the corresponding component; i.e., the component that was added to the layout with a constraint that matches the Leaf node's name. If no such component exists, then 0x0 is used.
Divider
The preferred width/height of a Divider is just the value of the MultiSplitLayout's dividerSize property.
MultiSplitLayout's floatingDividers property, initially true, is set to false by the MultiSplitPane as soon as any Divider is repositioned. When floatingDividers is false, the right/bottom edge of each Leaf (component) is defined by the location of the Divider that follows it. In other words, once the user moves a Divider, the layout no longer depends on the preferred size of any components; it's defined by the current position of the Dividers and the weights.
The layout algorithm requires two passes. If floatingDividers is true, the first pass sets the bounding rectangles of all of nodes to their preferred sizes according to the rules for Split nodes defined above. If floatingDividers is false, then we set the bounding rectangles of all of the Split/Leaf model nodes so that they occupy the spaces defined by the dividers. The second pass grows or shrinks the layout, if the MultiSplitPane's size has changed. It also takes care of setting the bounds of each component to match the bounds of its Leaf.
What makes the layout algorithm challenging is that growing and shrinking are not symmetrical. To grow the layout, extra space is added to sibling nodes, according to their weights (if no weights are specified, the last sibling gets 100 percent). Shrinking is similar, so long as none of the nodes shrink below their minimum size. When there's not enough weighted space to absorb all of the reduction that's required, then all of the components are reduced using (implicit) weights based on their current sizes. In other words, when the layout starts to get cramped, the biggest components shrink the most.
Making MultiSplitPane Layouts Persistent
If a user invests some time configuring a MultiSplitLayout by dragging the Dividers around, then they'll reasonably expect the Dividers to appear where they left them when the application is restarted. The configuration of a MultiSplitLayout is defined by the tree model, which is easily read or written as an XML file using the Java beans XMLEncoder and XMLDecoder classes.
Here's a code fragment example that saves the MultiSplitLayout model. It's run when the application is about to exit.
XMLEncoder e =
new XMLEncoder(new BufferedOuputStream(
new FileOutputStream(filename)));
Node model = multiSplitPane.getMultiSplitLayout().getModel();
e.writeObject(model);
e.close();
The code that loads the MultiSplitLayout model is similar. When the application initializes, we check to see if the model was saved in a previous session. If it was, then we set the MultiSplitLayout's floatingDividers property to false, which means that the initial layout should not be based on each component's preferred size. If we don't manage to load a model (e.g., because this is the first time the application was run), then we create the model from scratch.
String layoutDef =
"(COLUMN (ROW weight=1.0 left (COLUMN middle.top middle middle.bottom) right) bottom)";
try {
XMLDecoder d =
new XMLDecoder(new BufferedInputStream(
new FileInputStream(filename)));
Node model = (Node)(d.readObject());
mspLayout.setModel(model);
mspLayout.setFloatingDividers(false);
d.close();
}
catch (Exception exc) {
Node model = MultiSplitLayout.parseModel(layoutDef);
mspLayout.setModel(model);
}
You can run an example that saves and restores the MultiSplitLayout model by pressing the orange launch button. Rather than storing the model's XML archive in a file under the user's home directory, which would require access privileges and signing the example app, we've used the JNLP PersistenceService API. You can see how by taking a look at the openResourceInput and openResourceOutput methods in Example.java. Figure 8 shows the restored layout.
Figure 8. Example 4: layout changes are persistent
Making an entire application GUI's configuration persistent is beyond the scope of this article; however, it's worth noting that one additional trick is used to restore the size of the example window. If the layout model is successfully loaded, the MultiSplitPane's preferred size is set to match the root of the model:
This is sufficient for a simple example application. Saving the the persistent state for an entire GUI can be the subject of a future article.
Summary
This article has focused on MultiSplitPane basics: how to use it, how the layout algorithm works, and how to save and restore layouts using the standard java.beans persistence API. MultiSplitPane does have other features and capabilities that you can learn about by surveying the Javadoc. For example:
It supports the continuousLayout property, as in JSplitPane, which defers layout until after the user has finished dragging a divider (or hits Esc to cancel the gesture).
Dynamic changes to the layout model are possible. For example, one could add/remove a Leaf or an entire Split, or make a coordinated change to a set of Dividers. Calling MultiSplitPane.revalidate() causes the managed components to be synched with the model.
MultiSplitPane is accessible; it overrides getAccessibleContext() to provide a custom AccessibleJComponent.
Hans Muller is the CTO for Sun's Desktop division. He's been at Sun for over
15 years and has been involved with desktop GUI work of one kind
another for nearly all of that time.
MultiSpitLayout method checkLayout() - Split must have > 2 children
2008-08-21 23:33:49 pitris
[Reply | View]
Hello,
I want add only one component into MultiSpitLayout because I want to have possibility to display/hide components using MultiSlitLayout.
The only possibility to achieve that is to remove all components from MultiSplitPane then to change model and finally to add the only one displayed component. But method checkLayout() throws exception.
if (split.getChildren().size() <= 2) {
throwInvalidLayout("Split must have > 2 children", root);
}
1) Is there any way to display/hide components added to MultiSplitPane?
2) What is the reason to throw InvalidLayoutException if split has less than 2 children?
need public method set divider color
2007-08-27 15:46:43 angelflaree
[Reply | View]
I find it'll be more useful to add some method that can set color for dividers directly. It won't cost more than a few lines of code:
e.g.
define a member variable:
Color dividerBackground;
then add a method:
public void setDividerBackground(
Color background){
dividerBackgroundColor = background;
this.repaint();
}
and change the
g2d.setColor(color.black)
in the defaultDiverPainter
to:
g2d.setColor(dividerBackground);
Detecting changes in the divider position
2006-10-12 16:05:03 knc
[Reply | View]
I've just started evaluating MultiSplitPane but I'm excited about using it so I don't have to write my own. I am creating an application that contains multiple plots/graphs, with each plot is on it's own split pane. When the split pane divider moves the plot should resize to fit the new space. I was wondering if it's possible to set up a listener for each split pane divder in a MultiSplitPane to detect changes?
Thanks!
Well done!
2006-09-01 18:22:55 shaunkalley
[Reply | View]
After reading your article and writing a few samples I decided to use it in an application I'm developing.
One note: I believe the first line of setContinuousLayout should be
Hi,
I managed to find a bug (a major one at that).
Dragging a divider between two Split nodes laid out horizontally (left to right) beside each other renders dimensions of both nodes to be inconsistent with the actual drag.
Even though one might not prefer to use two horizontal Split nodes side-by-side (instead of combining them to be one split node), I thought that this might fix some bugs (unknown) in the layout.
To reproduce, use the following code for the model:
MultiSplitLayout.Split split1 = new MultiSplitLayout.Split();
split1.setRowLayout(true);
split1.setChildren(Arrays.asList(
new MultiSplitLayout.Leaf("a"),
new MultiSplitLayout.Divider(),
new MultiSplitLayout.Leaf("b")
)
);
MultiSplitLayout.Split modelRoot = new MultiSplitLayout.Split();
modelRoot.setRowLayout(true);
modelRoot.setChildren(Arrays.asList(
split1,
new MultiSplitLayout.Divider(),
new MultiSplitLayout.Leaf("c")
)
);
NOTE:- Do not use parseModel() because it combines (COLUMN a (COLUMN b c)) into (COLUMN a b c).
I have a request/question. Do you have any idea of the difficulty of actually fixing the bug? If you do not have time to fix it, please do respond with a way of fixing it.
Is it java 1.4 compatible?
2006-03-29 02:28:42 jcparques
[Reply | View]
I get an error using all jws examples.
(I am using java 1.4)
Romain Guy made the diagrams using a Macintosh
app ... I'll have to ask to post the name.
Losing things
2006-03-24 06:19:03 pdoubleya
[Reply | View]
Hey Hans--Nice demo, one thing I've noticed is that I can move dividers around in such a way that components can get "lost" (sized to zero) and I have no way of seeing that. Some auto-resize behavior (double-click on a divider to resize) might be nice...but some way to indicate that components were now hidden would also be good. Hmm...Cheers Patrick
Losing things
2006-03-24 15:29:26 hansmuller
[Reply | View]
I agree. I've done the simplest thing, which is sometimes what
you want. However it can also be rather confusing; there's
no prima facie way to determine that one or more panels will
appear if you start dragging on a split bar. A right button
menu in the divider space might help, but most users would
never find it. A minimum size for panes (not the same is
Component.getMinimumSize()) would ensure that no pane ever
disappeared. However if the user is really trying to optimize
the available space, that's not perfect either. I'd
definitely be interested to hear suggestion about how best
to handle this. It might just be some options to enable
the developer to choose.
This handles very many layout problems I have had before.
Quirk when resizing
2006-03-23 20:56:15 scheide
[Reply | View]
In the multi pane (3rd example), I make the center buttons a bit smaller by dragging the right divider to the left. Now when I resize the entire window wider, the center buttons maintain their smaller size. But if I make the make the entire window wider and then smaller (drag the right window edge to the right and then back to the left), the center buttons revert to their preferred size.
Is that intentional and if so, what is the reason and logic behind it?
Quirk when resizing
2006-03-23 22:55:47 hansmuller
[Reply | View]
The behavior you reported wasn't intentional, it's a bug. The column in the center should stay where you left it when you shrink the window. Thanks for report; I will fix that!
Quirk when resizing
2006-03-23 20:55:46 scheide
[Reply | View]
In the multi pane (3rd example), I make the center buttons a bit smaller by dragging the right divider to the left. Now when I resize the entire window wider, the center buttons maintain their smaller size. But if I make the make the entire window wider and then smaller (drag the right window edge to the right and then back to the left), the center buttons revert to their preferred size.
Is that intentional and if so, what is the reason and logic behind it?
Very nice!!!
2006-03-23 17:20:32 rameshgupta
[Reply | View]
This is a very useful component in and of itself! However, I am thinking of additional uses beyond what you've already touched upon in this article. For example, putting multiple table views sharing the same table model into a MultiSplitPane could, in theory, allow users to see disjoint areas of the table at the same time. It'd be awesome if you considered this as a possible future enhancement!
Thanks for this Hans. I've struggled with nested JSplitPanes in the past and am starting down that road again now. I was going to implement a MultiSplitPane myself but now I don't have to.
Does this have a home?
2006-03-23 07:42:39 coxcu
[Reply | View]
Is there a project page somewhere for questions, comments, RFEs, bug reports, etc...?
Does this have a home?
2006-03-23 09:01:20 hansmuller
[Reply | View]
I hope to get the code committed to the SwingLabs project today.
That's where the project will live longer term and where we'll publish
updates and fixes. You can provide provide feedback about
MutliSplit{Pane,Layout} on the
SwingLabs java.net forum or right here if you like.