A JavaFX Custom Container
I recently needed a JavaFX layout component that could display its children in a horizontal grid, where the individual controls could be resized. Take, for example, a set of lists of data arranged horizontally, where the user can click on a gap between the lists and drag to resize the associated list. Although JavaFX 2.0 consists of some layout components that come close to providing this functionality, none of them fit the bill exactly.
For example, the HBox layout spaces its children controls evenly across a horizontal region automatically, as shown in Figure 1. Simply position and size the HBox, and the controls are laid out for you.
Figure 1 — An HBox container lays out components horizontally
The code for this is very simple: Create the components you want laid out horizontally, and then add them to the HBox in the order you want them displayed. Here's an example:
HBox hbox = new HBox();
Scene scene = new Scene(root, 300, 250);
ListView list = new ListView();
Button btn = new Button();
Label label = new Label("JavaFX Rules!");
TextField text = new TextField();
btn.setText("Press me!");
hbox.getChildren().addAll(list, btn, label, text);
This is a good start, but the SplitPane container comes even closer, as it provides an actual splitter line between the controls that you can click and drag to resize (see Figure 2).
Figure 2 — A SplitPane container provides a splitter between controls to resize
The code is similar to an HBox, except for the additional line of code at the end to set the divider positions:
SplitPane sp = new SplitPane();
sp.setPrefWidth(scene.getWidth());
sp.setPrefHeight(scene.getHeight());
ListView l1 = new ListView());
ListView l2 = new ListView());
ListView l3 = new ListView());
sp.getItems().addAll(l1, l2, l3 );
sp.setDividerPositions(0.3f, 0.6f, 0.9f);
However, this container has some shortcomings: You're forced to use the splitter as is, and it doesn't provide enough flexibility in how child controls are resized when the parent container is resized. Also, you need to specify the locations of the divider in percentages. I want a container that automatically divides the components regardless of how many I place within it. To achieve exactly the visuals and behavior I was looking for, I set out to create my own JavaFX layout container to use in my applications. Hence, the SizeView container was born.
The SizeView Container
As I contemplated how to do this, I decided to just extend HBox, where I would place a Rectangle shape object as a divider in between each component added to the container. I could make the divider transparent to appear as an invisible gap between the components, or give it a color to be more explicit. Either way, you simply click on this space between the components and drag the mouse to resize (see Figure 3).
Figure 3 — The SizeView container, derived from HBox
The only tricky part was to get the behavior I was looking for as you drag a divider. For instance:
- When you drag a divider to the right, the component directly to the left of it grows in size, and the components on the right become equal in size and shrink together. Before and after:
- When you drag a divider to the left, the component directly to the left of it shrinks in size, and the components on simply grow equally. Before and after:
Here's how it works. When the SizeView container is created, it adds a listener for changes to the underlying HBox's children components. As components are added or removed, the divide() method is called for each. This method adds the Divider component (which extends javafx.shape.Rectangle) in between the controls. The code for this is below:
public class SizeView extends HBox {
boolean ignoreChanges = false;
public SizeView() {
setManaged(true);
// Listen for changes to the pane's child nodes
ObservableList<Node> children = this.getChildren();
children.addListener(new ListChangeListener<Node>() {
public void onChanged(ListChangeListener.Change change) {
if ( ignoreChanges )
return;
ObservableList<Node> c = getChildren();
if ( c != null && c.size() > 0 ) {
Control[] cs = new Control[ c.size() ];
cs = (Control[])c.toArray(cs);
divide(cs);
}
}
});
}
protected void divide(Control[] controls) {
// There needs to be at least one control in the array
if ( controls.length == 0 )
return;
ignoreChanges = true;
// Place each control (with a divider) into the hbox
for ( int i = controls.length-1; i > 0; i-- ) {
Control control = controls[i];
if ( i > 0 ) {
// Add a divider first, and assign it the control
// to the left to resize when moved
Divider div = new Divider( this, controls[i-1] );
div.setHeight( this.getHeight() );
getChildren().add( i, div );
}
HBox.setHgrow(control, Priority.ALWAYS);
}
ignoreChanges = false;
}
}
Since adding the Divider to the container in response to a changed event leads to more changed events, the ignoreChanges flag is needed to prevent an endless loop. Otherwise, this is very straightforward. The real work is done in the Divider class; here's a snippet:
public class Divider extends Rectangle {
public final Control control;
Vector<Control> leftControls = new Vector<Control>();
Vector<Control> rightControls = new Vector<Control>();
HBox parent;
// ...
public Divider(HBox parent, Control c) {
this.control = c;
this.parent = parent;
setWidth(SEP_WIDTH);
setStroke(Color.TRANSPARENT);
setFill(Color.TRANSPARENT);
setOnMouseEntered(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
setCursor(Cursor.W_RESIZE);
}
});
setOnMouseExited(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
setCursor(Cursor.DEFAULT);
}
});
setOnMousePressed(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
processChildren();
// Lock the left components
for ( Control control: leftControls ) {
double w = control.getWidth();
control.setMinWidth(w);
control.setMaxWidth(w);
}
oneTimeProc = false;
}
});
setOnMouseDragged(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
draggingRight = false;
// A positive relative X position indicates the user
// is dragging to the right. Do this once per click
if ( oneTimeProc == false && me.getX() >= 1 ) {
draggingRight = true;
oneTimeProc = true;
}
if ( draggingRight && ! preserveResize ) {
for ( Control control: rightControls ) {
double w = control.getWidth();
control.setMinWidth(Control.USE_COMPUTED_SIZE);
control.setMaxWidth(Control.USE_COMPUTED_SIZE);
}
}
double newWidth = control.getWidth() + me.getX();
if ( newWidth > control.getWidth() ) {
control.setMaxWidth(newWidth);
control.setMinWidth(newWidth);
}
else {
control.setMinWidth(newWidth);
control.setMaxWidth(newWidth);
}
}
});
setOnMouseReleased(new EventHandler<MouseEvent>() {
public void handle(MouseEvent me) {
draggingRight = false;
}
});
}
//...
}
The resizing logic is contained within the mouse-processing code. When the divider is clicked on, the components to the left of the control this divider manages are locked by setting their min and max widths to their current size. This prevents them from resizing at all. Next, as the mouse is moved, all of the components to the right are set to use the computed size of the HBox container, and the component immediately to the left of the divider is resized by as much as the mouse was dragged. As a result, the HBox automatically resizes the components to the right. It's quite simple, actually, thanks to the JavaFX HBox container.
You can find the complete code for the SizeView container with a test application on my site here.
Happy coding!
—EJB

