After my previous post, praising the properties and binding mechanism, I’d also like to voice an opinion on something on which I think the JavaFX team has dropped the ball.
In the previous post about Agenda control I explained that there were two ways I updated the nodes in Agenda; either I setup binding in the constructor, or I listen to relevant properties and call a relayout() method, setting the appropriate values there. Especially the binding is a powerful feature. No software engineer can deny that the sniplets below don’t have a certain beauty to them.
Adding a rectangle that serves as a dragger at the bottom of an appointment:
durationDragger.xProperty().bind(widthProperty().multiply(0.25)); // 25% offset from the left border durationDragger.widthProperty().bind(widthProperty().multiply(0.5)); // 50% of the width of the appointment durationDragger.yProperty().bind(heightProperty().subtract(5)); // 5 pixels from the bottom border durationDragger.setHeight(3); // 3 pixels high
Making sure the day head and day body line up:
header.getChildren().add(dayHeader); week.getChrildren().add(dayBody); dayHeader.xProperty().bind( dayBody.xProperty()); dayHeader.widthProperty().bind( dayBody.widthProperty());
What is important here is that the x,y,w,h values are set directly on the children. The container does not do any layout, it simply draws its childeren where they say they want to be. This is called an ‘absolute’ layout. All other layouts (VBox, HBox, BorderPane, …) do not want you to set the x,y,w,h of the children, their added value is that they’ll calculate and set them for you.
But they do have other parameters that can be set to influence how the children are laid out. The strange thing is: some of those parameters are stored in the nodes themselves (like min/pref/max width and height) and some are stored in the layout (like alignment and margin). And such an inconsistancy often is an indication something is amiss.
But there are other signs that conceptually something is not quite right. Suppose you add a node to VBox, then you could end up with code like this:
myVBox.getChildren().add(node); node.setMaxWidth(Integer.MAX_VALUE); VBox.setHgrow(node, Priority.ALWAYS); VBox.setMargin(node, new Inset(...));
Note the use of a static method to set a constraint for an a child. It’s not a method on the actual pane instance, but a static method. I’ve used such methods as well; usually to hide away an internal storage, for example a webapp’s request or session attributes. And that is what happens here as well; there is an internal constraint storage which is being set.
Now, another thing that people often stumble over when using JavaFX is the setMaxWidth() line. According to JavaFX’s layout managers this denotes the node’s intent to grow, because if it is set to a larger value, then the node apparently wants to be that value… Ahm. Isn’t that what the prefered size is for? As I see it, the maximum size value indicates the maximum size a node can still be sensibly drawn. It’s meta information, more of a limitation, not an intention. For example a slider: it can be stretched horizontally to unlimited (it’s not practical, but what is drawn still makes sense), but vertically it cannot be stretched. So a slider’s max width is unlimited, but it has a small max height.
So my issues are:
- Inconsistency; constraints are stored partially in the node and partially in an obscure constraint storage inside the layout.
- No API; setting constraints is not an uniform and intuitive API, there are unusual constructs required (calling static methods) to set some of the constraints.
- Properties that tell something about limitations are used for intention.
I feel the code sniplet above should actually have been something along the lines of:
myVBox.add(node, new VBox.Constraints() .vgrow(Priority.ALWAYS) .margin(new Inset(...)) );
Basically it boils down to this:
All administration involving the layout of a node should be kept in one place. This means that Pane can use the x,y,w,h properties in the node, for all other layouts the constraints should be provided in a separate constraint class
This is not something new; Swing does it this way, and back in 2011 the author of MigLayout and little ol’ me tried to convince -I even begged- the JavaFX team that this is the right concept. We were unsuccesful, unfortunately. Nevertheless I decided to rebel and used this approach in the port of MigLayout for JavaFX, also known as MigPane. The example sniplet looks something like this when using MigPane:
myMigPane.add(node, new CC() .growx() .pad(...) );
Beside the clear usage of a constraint class (CC), MigPane also ignores the max size values of certain nodes and controls, because initially their max size is set equal to their pref size, thus causing a grow-constraint to not grow the nodes. More examples can be found in a previous blog and in MigPane’s Git repo
MigLayout is a very popular layout manager for Swing and I think it could also be for JavaFX. MigPane is part of the official distribution of MigLayout. You need to download the core and the JavaFX specific artifact from Maven central or add the dependency to the JavaFX artifact to your pom. And let me know if there are any issues; MigPane still needs a good shake down.