5 Sep 2024

Understanding the Layout Process in Qt Widgets

I’ve been mostly involved with systems programming during my career, and only eventually I had to deal with user-facing applications. But since starting my work on Cahier roughly a year and a half ago, the balance has shifted towards user-facing software, so I’ve resumed my studies on UX and UI. The UI framework I chose to develop Cahier with is Qt, more specifically the Qt Widgets toolkit, because of its good support for desktop applications. While the road hasn’t been without bumps, I’m satisfied with what I’m achieving and have been writing about some of the core concepts behind the toolkit. To follow up on my Qt Widgets Rendering Pipeline article, I’d like to explore this toolkit’s take on layout handling.

UI frameworks are code libraries that provide input management, UI widgets and rendering APIs for application developers. Along the lifecycle of a UI, these widgets have to be shown and arranged on the screen according to the intentions of the developers, and frameworks have evolved some mechanisms for describing this arrangement in an abstract way. Once the descriptions are in place, the toolkit becomes responsible for positioning and resizing the elements. This is what we call layout management.

But to approach this subject smoothly, it is instructive to consider how widget arrangement works when developers have to handle it manually, without using the more abstract facilities provided.

Manual Layout

To illustrate the simplest case, we will start by creating a window and inserting a list view widget and a button at positions (6, 6) and (6, 242). Those widgets are positioned by calling the move method with a pair of coordinates (X, Y).

We do not need to set initial sizes, because widgets in Qt Widgets come with sensible sizes out of the box (provided by the member function sizeHint in the QWidget class). Each control type is responsible for defining its sensible size, generally based on the content it has to display. Here’s how it works for some widgets:

Moments before a widget is shown to the user, it will adjust its effective size (its actual size in the screen) to match its sensible size, making a call to sizeHint, thus relieving the developer from having to do it himself. Anyhow, aside from certain elements like buttons or labels, this is rarely the final size that the developer wants for the control, so he will have to set their size manually.

Since we haven’t instantiated any mechanisms for handling resizes or content changes, when the user resizes the window or when a button changes its text, sizes and positions will stay the same. This is how we used to code in older GUI frameworks like Win32, where positions and sizes had to be manually adjusted when the window received a resize event.

Automatic Layouts, or QLayouts

But there is a more expressive way of tackling this problem. Qt Widgets supports installing a layout manager, represented by the QLayout class, inside a widget, which makes the framework handle positions and sizes automatically for us. It increases the abstraction level of the layout by providing a way for developers to express the relative positions and resizing behavior for each child control. Widgets can be laid out horizontally (with QHBoxLayout), vertically (QVBoxLayout) or in a grid (QGridLayout). If we should attempt to describe layout management in Qt Widgets in one sentence, we could say that it offers layouts with layout-defined arrangement and child-based size hints, constraints and resize definitions.

Inner positions and sizes

First we’ll see how a horizontal or vertical layout, generically called a box layout, determines widget sizes. When defining the sizes for items along the main orientation axis (that is, x for horizontal layouts and y for vertical layouts), the layout first gives widgets the minimum and fixed sizes that they ask for, and then distributes the remaining space among all children. The remaining space is given to each item to approximate their sizeHint. Once all items have their sizeHint satisfied, they will grow in proportion to the GrowFlag or ExpandFlag set in their sizePolicy and the multiplicative factor stretch. In the opposite orientation (or cross-axis), box layouts make items either occupy their minimum sizes, or grow if they have the GrowFlag or ExpandFlag set, as much as their maximum size allows, following an alignment specified.

Then the item positions are trivial to determine once sizes are calculated. In the main orientation axis, you just have to add the layout margin, spacing and size of previous items to position a specific item. In the opposite orientation, you just add the margin of the layout to the position of the item.

Outer size

Layouts can either be installed in a widget, or added directly as an item inside another layout, thus creating recursive layouts and enabling developers to compose UIs with regions that have different arrangements. Either way, the layout now behaves not only as a container of children items, but also as an item inside another container. And the way items influence the layout process is mainly by specifying size hints, constraints and resizing behavior. A layout behaves differently from a widget because a layout doesn’t specify these parameters directly, but derives them from the widgets and sub-layouts contained in it.

In the case of an empty box layout, its sizeHint will be equal to the sum of the margins that the layout has, which by default is 11 pixels on all sides, adding up to a total of 22x22. The minimumSizeHint follows the same logic, to the effect that it assumes that the layout cannot be resized to a size smaller than its margins. Then for a layout that has children items, the hinted size in the main orientation (width for horizontal layouts or height for vertical layouts) will be the sum of the hinted sizes of all children widgets, and the hinted size in the opposite orientation will be equal to the biggest individual hinted size (width or height, depending on the axis) of its children. In other words, the layout calculates the bounding rectangle that its content would have when the layout is active, both for the sensible and the minimum size.

As soon as layouts are installed in a widget, they become responsible for managing that widget’s size behavior. So by default calls to a widget’s sizeHint and minimumSizeHint are effectively forwarded to the installed layout. Earlier we said that widgets have their initial size adjusted to their sizeHint, so this means that when a widget is created and has a layout installed, its initial size will be determined by the layout.

Layout flow

Apart from business logic and rendering code, layout is the third major CPU hog in GUI applications. Because of that, they are activated only when they are needed, through events received from the OS or user code. Some conditions that trigger a layout recalculation are: Showing a widget for the first time. Resizing a widget. Changing the content of a layout (new child added, child removed, or a child widget has changed its text, etc.). User code explicitly requesting recalculation.

When an item changes, it will invalidate its cache and post a recalculation request (represented by the LayoutRequest event) to the widget that contains the top level layout. Then when this event is handled, the layout enters a two phase process. It first determines the size hints and policies of all items by traversing the layout sub-tree (whose root is the top level layout and leaves are widgets), querying items for their sizes and recalculating them if their cache is invalid. The size hint and policy of layouts will also be calculated again based on their children.

Then the layout will start another traversal, but this time actually positioning and resizing children items. The top level layout uses the widget’s current size (or new size in a resize operation) as the space available, then it runs its algorithm according to the rules we described earlier, and sets the effective position and size of each child. Widgets will be positioned and resized accordingly, while sub-layouts will enter a recursion, arranging their own items in the space just made available to them.

Ultimately it’s the user who controls the outermost size of the UI in desktops, which is defined by the top level window. When the user resizes it, the top level widget receives the Resize event in order to accommodate its content in the size dictated by the user.

Conclusion

The layouts used in web technology (e.g. CSS flexbox) are much more covered than the ones provided by GUI libraries, so I hope to have helped tilt the balance a little in favor of GUI libraries. Besides, writing thorough explanations of how existent GUI frameworks work helps new GUI frameworks avoid reinventing concepts that work well, and instead build on already acquired knowledge.

Discuss on Hacker News and /r/cpp.