JavaFX2.0 Beta is out. We’ve taken advantage of the language shift to extensively remodel the layout APIs based on a year’s worth of tire-kicking. This article (the first in a series) will introduce you to the basics.
In marrying the GUI to a scene-graph, each graphical element is represented by a stateful object rather than drawn inside a clipped rectangle, and this makes layout more complicated. Hence, many of the JavaFX1.3 concepts still apply, however we’ve worked hard to simplify and eliminate the seams.
An interface is created by assembling scene-graph nodes into a hierarchy within a Scene. The three Parent types have different layout characteristics:
- Group: doesn’t perform any positioning of children and is not directly resizable (see next section) and just takes on the collective bounds of it’s visible children. Groups are most suited when you need to statically assemble a collection of nodes in fixed positions and/or need to apply an effect or transform to that collection.
- Region: base class for all general purpose layout panes; is both resizable and stylable via CSS. Supports dynamic layout by sizing and positioning children during a layout pass (if needed). If a Region subclass (layout pane) supports the layout paradigm you need, then use it instead of hand-positioning with a Group.
- Control: the base class for all skinnable controls. Is resizable and subclasses are all stylable via CSS. Controls delegate layout to their skins (which are Regions). Each layout Control subclass provides a unique API for adding content in the appropriate place within its skin; you do not add children to a control directly.
A silly example to give you the flavor if you’ve never programmed FX:
// silly example Text text = new Text("JavaFX 2.0!"); text.setFill(Color.RED); text.setFont(new Font(24)); Line line = new Line(2, 8, 104, 8); line.setStroke(Color.BLUE); line.setStrokeWidth(4); Group group = new Group(); group.setEffect(new DropShadow()); group.getChildren().addAll(text, line); BorderPane root = new BorderPane(); root.setTop(new Label("Introducing...")); root.setCenter(group); Scene scene = new Scene(root); stage.setScene(scene); stage.setVisible(true);
To explore some core scene-graph layout concepts, let’s zoom in on the key classes:
Each Node in the scene-graph has the potential to be “resizable”, which means it expresses it’s minimum, preferred, and maximum size constraints and allows its parent to resize it during layout (hopefully within that range, if the parent is honorable).
Each layout pane implements its own policy on how it resizes a resizable child, given the child’s size range. For example, a StackPane will attempt to resize all children to fill its width/height (honoring their max limits) while a FlowPane always resizes children to their preferred sizes, which is what we mean by the term “autosize” (resize-to-preferred).
But here’s what surprises (and annoys) people: not all scene-graph node classes are resizable:
- Resizable: Region, Control, WebView
- NOT Resizable: Group, Text, Shape, MediaView*, ImageView*
*will be made resizable in future release
If a Node class is not resizable, then isResizable() returns false, minWidth()/minHeight(), prefWidth()/prefHeight(), and maxWidth()/maxHeight() all return its current size, and resize() is a no-op. The size of a non-resizable node is affected by its various properties (width/height for Rectangle, radius for Circle, etc) and it’s the application’s responsibility to set these properties (and remember that scaling is NOT the same as resizing, as a scale will also transform the stroke, which is not usually what you want for layout). When non-resizable nodes are thrown into layout panes, they will be positioned, but not resized.
You inevitably want to ask why not make shapes resizable? While it’s straight-forward to do the math on basic shapes (Rectangle, Circle, etc), it gets a bit muddier with Paths, Polygons, and 3D. Also, keeping the core graphics shapes non-resizable allows us to make certain efficiencies, like ensuring a Group with non-resizable descendents pays no penalty for layout. So for now anyway, that’s the way it is.
Tip: if you need a resizable rectangle, use a Region and style it with CSS.
For most node classes, width and height are independent of each other, however, there are a handful where height-depends-on-width or visa-versa. In order to deal with these guys properly, the layout system needs a way to decipher it, hence Node’s getContentBias() method. This returns null by default, which means there is no width/height dependency and layout code can pass -1 as the alternate dimension to the minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), and maxWidth(h)/maxHeight(w) methods.
Labeled controls have a HORIZONTAL content-bias when wrapping is on. FlowPane and TilePane support a content-bias that matches their orientation. Note: turns out that HBox, VBox, and StackPane return a null content-bias, however they should really report a content-bias based on their childrens’ content-biases; this is a bug that will be fixed before the final release.
Our layout classes do the heavy-lifting in supporting content-bias in the system, so unless you are writing a custom layout class that handles arbitrary nodes, you needn’t deal directly with this API. Just know layout supports content-bias automatically when you build your interfaces.
Overriding Min/Pref/Max Sizes
Most Control and layout pane classes are pretty good at computing how big they ought to be based on their content and property settings — we’ll call this their “intrinsic” min, pref, and max sizes. For example, a Button will compute these sizes based on its text, font, padding, and so on; additionally a Button’s intrinsic max size defaults to its preferred because you usually don’t want buttons expanding to fill space. You shouldn’t have to explicitly set the size of controls, layout panes, or even the Scene, because the system will shrink-to-fit around your content.
But that’s a lie. Or at least counter-intuitive to the nature of designers. Thus, you can directly influence the size of Controls and Regions by setting their min, pref, and max size ‘override’ properties:
// make my button 100 pixels wide! button.setPrefWidth(100);
By default these properties have the sentinel value USE_COMPUTED_SIZE, which causes Node’s minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), maxWidth(h)/maxHeight(w) methods to return their intrinsic values. Setting them to a positive double value will override their intrinsic values.
Often you want to ensure that the min or max size tracks the preferred, so the min and max overrides may also be set to USE_PREF_SIZE, which does the obvious:
// clamp my stackpane's size stackpane.setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
Beware of the subtle different between the Node methods and the size override properties:
button.getPrefWidth() returns the size override (might be a magic sentinel value) while button.prefWidth(-1) returns the actual preferred width of the button (returning the override if set). Layout code should always use Node’s minWidth(h)/minHeight(w), prefWidth(h)/prefHeight(w), maxWidth(h)/maxHeight(w) methods for layout computations.
The Beauty of Beta
There’s more to the story of course, but I think that’s one blogful enough for now.
With the JavaFX2.0 public beta, you have the opportunity to influence the API based on cold, hard use cases, so don’t hold back (as if you would :-)).