Channels ▼

Eric Bruno

Dr. Dobb's Bloggers

A JavaFX Custom Container

December 05, 2011

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.

HBox 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).

SplitPanel 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).

SizeView 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:

  1. 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: Before After Right

  2. 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: Before After Left

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

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 


Video