Tuesday, 16 April 2013

Qt Layout Tutorial - Part I

Note: Now that the tutorial is finished, here are links to part II, part III, and the files.

Laying out a roadmap

Over the next few posts, I'll show how to get Qt's layouts to do what you want, instead of totally ignoring you and just "doing their thing".

I'll start by saying what this is not:
  • It's not a reference of Qt's widgets/layouts.
  • It's not an introduction to Qt's layouts.
In fact, it may not even be the best/cleanest way to achieve the result I present here. It's just the way I figured out. It's also the tutorial I wish I had found when I had to deal with layouts in Qt, namely, something that explained step-by-step how to create a non-trivial UI in Qt Designer.

We'll be creating an UI similar to this (riveting, heh?):

|       ||    |
|   A   ||  B |
|       ||    |
|      C      |

A - Visualization area. When we resize the form, this area expands/shrinks both horizontally and vertically. In this tutorial, it'll probably contain a QLabel (e.g., for image viewing).
B - Input area. This will contain two group boxes, each with several widgets. When we resize the form, this area will move, but won't grow/shrink. This area will anchor on the form's left side.
C - Feedback area. I have nothing defined for this area, and will improvise a bit once we get to it. This area will grow/shrink horizontally, will move vertically, and it will anchor on the form's bottom.

By the way, when I use the word "form", I'm referring to any of the options we can use for a .ui file in Qt - main window, dialog, page, widget, etc.

Let's get started

1. We'll start by creating a new GUI project in Qt Creator. As I said, this is not an introduction, so I expect you know how to create a GUI project in Qt Creator.

2. Add 3 group boxes to the form, with positions roughly similar to areas A, B, and C, above.

3. Add 2 more group boxes to area B, one on top of the other; we'll call them B1 (top) and B2 (bottom). Add a vertical spacer below B2. Since area A will grow vertically and area B won't, we'll use this spacer to fill the remaining space.

4. Add the following to B1, in 2 rows:
  • Top row: label + line edit + label + line edit + horizontal spacer.
  • Bottom row: 3 push buttons.

5. Edit the labels and input your description. You can then resize the labels by selecting them and pressing Ctrl+J. Or right-click and select "Lay out -> Adjust Size". Notice that the labels will adjust to the text you've entered.

6. Next, resize the line edits to a sensible width for what you expect to input. E.g., in my case, I'll be inputting numbers in the range 0-255.

7. Now, we'll apply our first layout. Select the top row (label + line edit + label + line edit + spacer) and apply a horizontal layout. What happens next depends on the width of the form and the widgets; in my case, the labels kept their width, but the edits increased in width and threw the spacer off the form, to the right.

So, what's going on? If you take a look at the labels, their sizePolicy is "Preferred, Preferred". However, for the edits, sizePolicy.HorizontalPolicy is "Expanding". And maximumSize.Width is someHugeValueDefinitelyLargerThan42 (16777215, in my case). We need to put some sane values here.

First, press Ctrl+Z to undo the layout.

We'll start with the labels. Even though they kept their size, I believe it's cleaner to change them to state our intent - change their maximumSize.Width to match their current size, i.e., geometry.Width.

Next, we'll change the edits, setting sizePolicy.HorizontalPolicy to "Minimum", and maximumSize.Width to each edit's geometry.Width.

Now, we reapply the horizontal layout. Et voilĂ . The madness is gone, everything's sane. Well, for now. There's still one change missing, and we'd better do it now. Why? Because if we don't do it now, when we have just a few widgets, we'll have to go through a most-certainly-not-boring-no-no-not-at-all cycle of trial and error later, when we have, say, some 20+ widgets on our form.

So, select the spacer and change its sizeType from "Expanding" to "Minimum". And why are we changing this? Because if we don't, when we apply a layout to the form, area B will expand, and that's not what we want.

So, to recap: After setting the labels and edits to your desired width, set their maximumSize.Width to that value, and set sizePolicy.HorizontalPolicy to "Minimum" for both edits and the spacer.

8. Now, we'll apply our second layout, on the 3 push buttons in the bottom row of B1.

Here, we won't have to do anything else. Even though each button's maximumSize.Width is someHugeValueDefinitelyLargerThan42, their sizePolicy.HorizontalPolicy is "Minimum"; this ensures that these buttons won't cause B1 to expand, which is our goal.

If you want to keep the buttons smaller, add 2 spacers between them. If you want to keep 2 buttons together and separated from the third one, add a spacer between them. Just remember that the spacers on B can't have sizeType "Expanding", as that'll cause B to expand later on. I'm using "Minimum" since it's what worked best for me.

A dose of reality

Even though the steps presented above are nice, clean, and simple, the process to get there was anything but. It involved generous amounts of trial and error, experimenting and fiddling.

I now have a better understanding of how to get the layouts to do what I want, but I still think this is not practical. We shouldn't have to fiddle with each control to get the results we want, there should be overriding policies at both the layout level and the container level. And Qt Designer should allow us to resize the widgets after a layout has been applied and should have options to automatically change the necessary values to keep the appearance we want within the layout.

Yes, I know. This is Open Source, if you don't like it, contribute. The thing is, my sentence above, "I now have a better understanding of how to get the layouts to do what I want", is only half the story; the other half is "but I still don't actually understand how layouts work, I still can't visualize all the parts that contribute to it and grok their interactions". And this is definitely not high on my list of priorities, nor should it be.

Nor should it be, and that is actually my main complaint about the layout managers I've had to deal with, both in Qt and in Java - for something that should be simple and transparent, they sure require a lot of investment just to get it working, once you move away from Trivialville.

Anyway, that's it for now. We'll continue this tutorial on my next post, when we'll add a boatload of widgets to area B2.

No comments:

Post a Comment