It’s not all gold and sunshine; JavaFX layout

  • Post category:Java / javafx / UI

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:

  1. Inconsistency; constraints are stored partially in the node and partially in an obscure constraint storage inside the layout.
  2. 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.
  3. 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.

This Post Has 7 Comments

  1. Nikolay

    Completely agree with claims against layout mechanics in JavaFX 2. Whole framework is cool, but layout approach is totally fail.

  2. Dmv

    Thank you both for a reasoned debate. As a relative newbie to JavaFX (coming from a totally different UI tool), I have often wrestled with the JavaFX layout sizing and growing options. Of course, as Jonathan says, it takes time for the concepts and details of any UI to sink in properly but I have to agree with much of what Tom has said here. Maybe I am not bright enough and I’ve spent too long doing things a different way but I learned long ago to shun complexity where there is an alternative. JavaFX is an amazing development and a very attractive solution to UI but there are areas where I always feel that I am have to walk as if I were treading on eggshells.

  3. tbeernot

    Thanks for responding Jonathan, it is highly appriciated.

    After rereading your post a number of times, I actually only see support for my claims. Take for example the first point about the static methods. You correctly say that cramming all layout constraints into one API would have been a bad idea; it indeed is. However, the next part illustrates that JavaFX layout does precisely that; trying to push all layout in one API, namely a map. And that is somehow made “eatable” with the static methods. And the cross-layout argument initially seems like a good point, but in fact it has a conceptual fault; it very possibly falsly assumes that if a certain constraint applies to the node in layout A, it also applies to the node in layout B… Maybe I do not want my node to grow in layout B?

    According to my concept the correct way would have been to provide two constraint sets (instances), one for layout A and one for layout B. The only issue is that you now have constraints that apply to a node that is not (yet) managed by the layout. So to facilitate this, only one simple addition is needed; a way to have the layouts hold constraints for nodes that are not part of the layout. So something like:
    layoutA.add(node, new LayoutA.Constraint()…);
    layoutB.setConstraints(node, new LayoutB.Constraint()…);

    No cramming, no maps, no static methods; a clean and typesafe API.

    About the setMax* methods; we agree that it should denote abilities and limitations, and that the layout’s constaints should hold intention. However the statement “if the maximum size weren’t clamped to the preferred size … why do all my nodes grow”, is a clear indication that max size in this is declaring a grow intent. Nodes should grow because it is declared so in the layout constraints, not because max size allows them.

    I my opinion growing should work like this:
    – if no grow constaint is set; try to draw the button at its preferred size
    – if a grow constraint is set: let the node grow to its max size

    In this scenario I see absolutely no need to have a max size that initially is clamped to pref size, nor do I see any of the discussions arise that you describe.

    Pushing that up a nudge: max sizes are informative; it tells the layout how far it can go. Basically the associated setters are not even required. If the user does not want a node to grow to its max size, than that is a fine tuning of the layout constraint. So instead of altering a node’s max size (which would be weird, since its capability to draw itself to a certain size does not really change), it should be declared as a refinement of the layout constraint (e.g. to a max of 100 pixels):
    myLayout.add(node, new Constraint().growH(100));

    I agree that certain things are difficult, especially when it comes down to the details. However, I’m also a strong believer that the core concept should be simple and clear. I’ve been involve with JavaFX since 1.0 and the layout concept still hasn’t landed fully. And it’s not because of lack of experience after 10+ years of doing UI in Java. The conceptual idea still has not sunken in. Maybe it is because I’m not bright enough, that may very well be. Maybe it is because in my mind I’m stuck on a single track and can’t see the other one, but at this point I’m not convinced I’m wrong.

    Last one; I know the deadline back in august 2011 was a big one, and I can understand not having time to alter it prior to the J1. But I would have taken a different approach; you were (are!) rolling out a new UI framework in an already crowded market. Better make it the best it can be, not a decent one. I won’t argue with the choices made, I wasn’t there, but I will point out and discuss technical issues I see, in the hope they will improve.

  4. Jonathan Giles

    I know that this is a contentious issue, but reading your blog post I can’t disagree with the approach that we’ve taken. I know anything I say will be torn to shreds, but I thought I would at least attempt to outline some reasons why I like the approach we have now. Of course, my point of view is both biased and informed by the many nuances of our decisions along the long journey that is JavaFX.

    Firstly, with the static methods from VBox, etc. The thing you may not realise is that when you set the property (e.g. VBox.setHgrow(node, Priority.ALWAYS); ), what you’re actually doing is setting a property inside the node’s properties map. This means that the Priority.ALWAYS is stored on the node, not in the VBox as you might think. The reason we did it this way was to 1) minimise the amount of API that would need to be exposed somewhere (imagine how bulky – and wrong – Node would be if it had to have API for all built-in layout containers), and 2) allow for a single node to be configured for multiple layout containers and be able to move between them without any additional reconfiguration (after the initial setup). This could potentially be very useful in a future where one animates between different layout containers, for example.

    Secondly, regarding the setMax* methods: you’re conflating ability to grow with actually growing, and confusing the purpose of pref and max sizes. Just because a maximum size is changed (to some MAX_VALUE, for example) doesn’t mean the node will grow – it simply indicates to its layout container that it can grow up to that size. It is up to the layout container to actually determine whether it will allow for the node to grow or not, and different layout containers have different approaches. If the maximum size weren’t clamped to the preferred size, we would hear the opposite complaint from people: “why do all my nodes grow to fill all available space! I want them to be their logical size.” If, alternatively, we simply did as you suggest above and have the layout container essentially ignore the max size and use the pref size, people would complain about having to change the pref size to a larger value for their node to grow automatically – but then what would happen when the layout container dynamically changed (this was, I believe, what Swing did – and by Amy’s own admission Swing was broken in this regard – it was never updated properly to use max sizes).

    Layout is by far one of the most complex areas of any UI toolkit. I have had the good fortune of being the technical lead in the team that developed the JavaFX layout APIs (although I was not lead whilst they were being developed), and have presented on the topic at many conferences. However, if it weren’t for this I know for a fact I would be confused about layout – it is just one of those things that requires many hours of time to be invested into it to best understand it. But, this is not specific to JavaFX – layout in any UI toolkit takes time to fully understand the nuances. I know it is a big ask for people to do this when they just want to write apps (and get paid!), but I think in general the approach we have is a decent one, and I think that whilst it is easy to shout your dislike about things, it is a lot harder in reality to get things done better without there being ramifications elsewhere. My job, as well as the rest of the JavaFX engineering team at Oracle, is to make the best darn API we can – we really do try our best to make something amazing. I was one of the people in the mailing thread that was raised back in August 2011 regarding the layout APIs – it was just way too late to be able to be acted on – there was already a lot of stuff built on top of it by that point (even though it was pre-2.0).

    Finally, like I said at the beginning, I know layout is a contentious issue, and one where there is often very little common ground between the various ‘camps’. I do not intend to enter any philosophical navel gazing as I don’t think anything of much value can come out of it. I think the APIs we have are decent, but I welcome additional layout containers such as MigPane – having options out there for the developers using JavaFX is great, and in the end this is why we do what we do – to make the life of programmers easier and more productive. There is room for all of our opinions, as long as we can continue to develop our own layout containers.

    1. Mikael Grev

      I need to chip in as well since this is a dear subject to me. When I firstly created MigLayout I started out, of course, by researching almost all other GUI frameworks on the market. I did this so that I could create a GUI agnostic API that could be ported to all frameworks. Swing and SWT was the first to be implemented but others would follow.

      Swing is not that bad when it comes to the layout framework actually. What lacked (severely) was good implementations of the layout managers. Swing basically had these problems:

      1) It didn’t have a clear way to ask “What would be your height given a xxx width”
      2) It doesn’t separate width and height. You can therefore not set the preferred width on a component without also setting the height, which you probably still want to be calculated but the button.
      3) There was no way to use anything other than pixels. Fine for 1998, but terrible now. And on the same subject if you wanted to create an app that should have white space per platform, to adhere to style guides, you were in a world of pain before MigLayout (I might be just so slightly biased here though).

      Swing tried to fix 1) by internally returning a min/preferred height that depended on the CURRENT width for multi-row text component. This was a huge mistake. It was a hack for a basic shortcoming and I don’t think I’m exaggerating when I say that this is the biggest problem to work around in MigLayout for Swing and also what users has most problem understanding. I have had to explain it to people, and what it means for growing components, more times that I can count.

      It is imperative to have a clean, simple well thought through layout framework or everything becomes a bunch of hacks for shortcomings.

      The static VBox thing is a fix for the fact that JavaFX hasn’t a clear notion of where to put the layout constraints. Here it lacks in the contract between the layout manager (Swing lingo) and the component. The fix is both faulty in OO perspective but it also fails the discoverability requirement on API, meaning there’s no easy way to find the method unless you already know about it. The reason about API size is, IMHO, bogus. If one start removing needed methods from an API to reduce the count, that only shows it is segmented wrong. The requirement to move a node between multiple layout managers is not at all common, and if it was, there are better ways to solve it.

      The maxSize thing is basically also a fix for the same problem. Here JavaFX max size IS an indication on growth, however you put it. That can’t be interpreted as anything other than a hack since nothing should grow unless the layout manager has been instructed to to grow it. Very few components should have a max size set since they can be rendered just fine in any size larger than the preferred one. One might want to limit the size for other reasons than component visuals though, usually because it doesn’t make sense to make it wider/higher than a certain size. When it comes to buttons there is no such size. When it comes to a aspect ratio locked components it might be, if the size in the other dimension is given.

      For instance, buttons should quite often grow to be the same width, which is more visually pleasing. Or, at the very least, grow to some 75px (or equivalent) to adhere to UI design guidelines of different platforms. Now you have to increase the size to a magic Integer.MAX_VALUE just to make it possible to grow.

      I don’t understand “If the maximum size weren’t clamped to the preferred size, we would hear the opposite complaint from people: “why do all my nodes grow to fill all available space! I want them to be their logical size.”. I have never ever heard that about Swing, which have this behavior.

      The problem in the API is that JavaFX hasn’t divided the land between the graphics oriented scene graph and normal UI component is a distinct way. The node count is normally at least two orders of magnitudes apart and therefore the requirements on speed. JavaFX mix and match thinking there exist one API to rule them both. It was a good intention but none, ever, wants to rotate a button other than once, for fun. I see basically the same mistake as when Sun decided that subclassing Container with JComponent was a good idea.

      I am actually surprised that the JavaFX team chose to go its own way in 2011 and not listen to those who have been doing this for a long time.

      Cheers,
      Mikael

  5. Swinger

    Too bad. If they let these kinds of designs through.. It makes me worried for the future of JavaFX since these kinds of things will never get fixed.

    1. tbeernot

      Well, back in august 2011 we weren’t talking to totally deaf ears, but there were other pressing issues, like the then upcoming J1 and JFX’s 1.0 release. And the team decided to not change it. Maybe we were a little late reporting it, but JavaFX was very much in flux at that time and we kinda were put off with all the changes we needed to make to our code after each release. But I think it indeed was a big mistake, because at that time the number of JFX applications were extremly limited. They could have stated that the layout manager would be getting an overhaul. It’s not even that big of a chance to the actual core. Other areas of JavaFX are very well though through thou.

      I am considering exploring if it is possible to wrap the layout managers in derrived implementations, that do the same as MigPane: configure by constaint and ignore the max size values.

Leave a Reply to tbeernot Cancel reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.