What makes Control components so magical?

Problem Statement

If you come from traditional UI toolkits such as GTK [1] or Qt Widgets [2] then the ultra-flexible way in how QML lets you position items and deal with layouts will be confusing. First, there isn't just one way but several layout strategies to choose from:

  • Direct positioning & widget sizing (via x, y, width, height properties),
  • Anchor layouts (anchors { ... }),
  • Positioner containers (Flow, Row, Column, Grid),
  • Dynamic layout containers (GridLayout, RowLayout, ColumnLayout),
  • View containers (ListView, GridView, ScrollView, ...).

The first two operate on the Item level, relative to their parent, whereas the other three types operate as containers, meaning you can place items into them and they'll take care of the layout.

This is all good and fine until you realize that there are hidden conflicts, depending on which layout strategies you use and mix. But it also depends on whether your components, custom or not, are well-behaved. In the best case, you'll run into binding loop warnings, and perhaps you'll ignore them (hint: Don't!). In the worst case, unhappy layouts crash your application. And you'll never be quite sure whether a new Qt release will change these layout semantics (worst offender: Dynamic layout containers).

Here's an example that shows how easy it is to create a binding loop:

ApplicationWindow {
    id: root

    title: "Binding loop on implicitWidth"
    visible: true

    ScrollView {
        id: view

        anchors.fill: parent
        contentWidth: view.contentItem.width + view.leftPadding + view.rightPadding
        leftPadding: 6
        rightPadding: 6

        Item {
            width: view.width
        }
    }
}

When run, the QML engine will show you this warning:

QML ScrollView: Binding loop detected for property "implicitWidth"

Unless you understand what's going on, this error message can be quite misleading! After all, we didn't even touch the ScrollView's implicitWidth property, and if we were to set it, the binding loop would magically disappear.

Here's a conflict created by mixing Positioner containers and anchors:

ApplicationWindow {
    id: root

    title: "Wrongly anchored Item inside a Row"
    visible: true

    Row {
        anchors.fill: parent

        Item {
            anchors.left: parent.left
            width: 60
            height: 40
        }
    }
}

And that's the error we get, which at least is expected if you've read the documentation:

QML Row: Cannot specify left, right, horizontalCenter, fill or centerIn
anchors for items inside Row. Row will not function.

Here's the dreaded updatePolish() error that sends your application into a livelock:

ApplicationWindow {
    id: root

    title: "Livelocked in updatePolish()"
    visible: true

    ColumnLayout {
        anchors.fill: parent

        ColumnLayout {
            implicitWidth: parent.width
            implicitHeight: parent.height * 3
        }
        ColumnLayout {
            implicitWidth: parent.width
            implicitHeight: parent.height + parent.children[0].height * 2
        }
        ColumnLayout {
            implicitWidth: parent.width
            implicitHeight: parent.height + parent.children[1].height * 5
        }
    }
}

Upon launching the app, you either get the error immediately or you'll see it after attempting to resize the application window:

 QML ColumnLayout: possible QQuickItem::polish() loop
 QML ColumnLayout: ColumnLayout called polish() inside updatePolish() of ColumnLayout
(repeated)

What a bummer! Apparently we have to be careful in how we nest components, and which properties to bind. What if there was a component that could act as a layout barrier, reducing the risk of harmful layout interactions? Something that would look a bit like this:

ColumnLayout { // outer layout, shielded from inner layout
    SomeLayoutBarrier {
        Row { // inner layout, shielded from outer layout
            ...
        }
    }
}

The anatomy of Control

Control [3] fits the concept of a layout barrier, partially due to its well-behavedness. It's essentially three components rolled into one:

  1. contentItem is where the content goes,
  2. background is mostly used for colouring,
  3. Control houses the other two and presents itself almost as a regular Item to the parent component.

Here is a simplified & slightly wrong example illustrating the basic idea of the Control component:

Item {
    id: control

    default property alias contentData: contentItem.data
    property real padding: 0

    implicitWidth: background.implicitWidth
    implicitHeight: background.implicitHeight

    Rectangle {
        id: background

        anchors.centerIn: parent
        implicitWidth: 540
        implicitHeight: 360
        color: "teal"
    }
    Column {
        id: contentItem

        anchors {
            centerIn: parent
            margins: parent.padding
        }

        Text { text: "Hello there!" }
    }
}

The same example, but instead of a poorly handrolled version we now use the Control component:

Control {
    id: root

    background: Rectangle {
        implicitWidth: 540 + root.leftInset + root.rightInset
        implicitHeight: 360 + root.topInset + root.bottomInset
        color: "teal"
    }
    contentItem: Column {
        Text { text: "Hello there!" }
    }
}

Not only does the reduction in boilerplate result in much cleaner code, it also becomes more flexible, in a good way:

  • Both background and contentItem are injectable and can be replaced for more customization,
  • Implicit sizing (or natural sizing) is derived from the background item (don't forget to account for the {left, top, right, bottom}Inset properties),
  • Explicit sizing is set on the contentItem by the Control, with no need to define anchors nor width or height ourselves,
  • The {left, top, right, bottom} x {Padding, Inset} cross product of properties can be used to add margins to either the contentItem or the background item.

The clear semantics offered by Control also makes for an extremely robust component when compared to Item or some of QtQuick's *View components. Neither background nor contentItem are directly affected by whatever layout strategy is used for the Control. Conversely, whatever layout strategy is used for the contentItem, its side effects are contained by the Control itself.

It's no surprise then that Control is the base component for almost all widgets you can find in QtQuick2. The contentItem property offers a great customization point for slightly quirky components such as the Loader (strange anchor semantics) or the ScrollView (strange implicit size semantics). Thus the containment effect helps us to wrap not-so-well-behaved components into properly well-behaving Control components. This increases composability and lowers maintenance costs.

The small overhead induced by the two extra components for each Control encountered by the QML engine is a good trade-off to take.

A playground for Control

Building the proper intuition for Control isn't easy; there is no replacement for playing with the component. Especially the multi-layered interactions of padding, inset, explicit size, implicit size and positioning can be confusing.

So let's build a playground for ourselves to study the Control component:

ApplicationWindow {
    title: "Control, background & contentItem"
    width: 1280
    height: 800
    visible: true

    Column {
        anchors.centerIn: parent
        spacing: 24

        Switch {
            action: Action {
              id: enableClipping

              text: "Enable clipping"
              checked: false
              checkable: true
            }
        }
        SomeControl {
            clip: enableClipping.checked
        }
    }
}

SomeControl will have handles on each edge of the contentItem (shown in orange) and the background (shown in teal) that we can drag to change either padding or inset properties. It'll also display the current geometry for both items. The geometry of the Control is not affected by dragging the handles, which also explains the containment effect of our layout barrier:

Dragging the handles reveals how everything interacts

The drag handles allow us to set negative values to each padding or inset property. When this happens, SomeControl will paint outside its bounds. In QML, each item is responsible for painting itself properly. The QML engine will not enforce painting within bounds unless clip: true is set. We use a switch to quickly see the effect of clipping vs. no-clip (default).

We notice that changing the leftPadding also changes contentItem.x, on top of the width, and topPadding affects contentItem.y on top of the height. When rightPadding or bottomPadding is changed however, only the width and height of the contentItem change. We can observe a similar effect for the background when changing the insets.

This is the basic structure of SomeControl:

Control {
    id: root

    background: Rectangle {
        color: "teal"
        implicitWidth: 640 - root.leftInset - root.rightInset
        implicitHeight: 360 - root.topInset - root.bottomInset

    }
    contentItem: Rectangle {
        color: "orange"
    }
}

As mentioned before, we do not have to worry about explicit sizing for the Rectangle that is set as our contentItem. The implicit (or natural) sizing however is informed by the background, and we are responsible for mimicking the explicit size behaviour of the contentItem.

Q: What would happen if we had forgotten to substract the insets from implicitWidth and implicitHeight?

A: The background's size would remain constant, meaning that changing the insets would result in moving the background.

You can experiment with the playground yourself, the full code example [4] is available on GitHub. Here is a short video of the application:

Video: Changing paddings and insets affetcs contentItem and background

Summary

We explored the Control component under the aspect of common QML layout issues and how to avoid them. Typical layout tasks can be simplified and its well-behavedness counters the side effects that are usually responsible for these issues. All that is required is to apply the patterns laid out for us, and to fully embrace the flexibility of the component. If done correctly, each Control component becomes a layout barrier, a concept & definition that we introduced in this article.