This is a text-only version of the following page on https://raymii.org:
---
Title : Responsive QML Layout (with scrollbars)
Author : Remy van Elst
Date : 05-10-2021
URL : https://raymii.org/s/articles/Responsive_Qml_layout_with_scrollbars.html
Format : Markdown/HTML
---
![responsive screenshot][1]
> Screen recording of a responsive `GridLayout` in a `ScrollView`
In this article I'll show you how to make a responsive layout in Qt / QML that automatically adjusts the amount of columns and rows based on the window dimensions, including scrollbars for when the content does not fit inside the window. This also works if you have a portrait and landscape orientation of your application, since the screen or window dimensions will be different across those two builds. I also explain how the dynamic resizing works with an explanation of property bindings in QML and as a bonus this works on mobile (Android/iOS) as well.
Recently I removed all Google Ads from this site due to their invasive tracking, as well as Google Analytics. Please, if you found this content useful, consider a small donation using any of the options below:
I'm developing an open source monitoring app called Leaf Node Monitoring, for windows, linux & android. Go check it out!
Consider sponsoring me on Github. It means the world to me if you show your appreciation and you'll help pay the server costs.
You can also sponsor me by getting a Digital Ocean VPS. With this referral link you'll get $200 credit for 60 days. Spend $25 after your credit expires and I'll get $25!
QML is a markup language (part of the QT framework) like HTML/CSS, with inline
JavaScript that can interact with the C++ code of your(QT) application. QML
has the [concept of][3] `Layouts` to arrange items in a user interface. You
can have a `RowLayout` for, unsurprisingly, a row of items, or a
`ColumnLayout` for a column of items. `GridLayout` is the most flexible, that
allows for a grid of items. There is also the `StackLayout`, where only one
item is visible at a time. You must specify te amount of rows and columns, but that
does not change when a user resizes the window. This means that the layout
is not responsive.
A responsive layout means that when the window dimension (or device rotation aspect)
changes, the contents of said window automatically reposition themselves in a way
that fits best. Like how modern websites look great on your desktop and phone, using
a different layout for each device. In Qt / Qml this is possible, but not by default.
Here are two pictures that show off a `RowLayout` and a `ColumnLayout` to
help you visualize the concept:
![rowlayout][7]
> RowLayout with 2 rectangles
![columnLayout][6]
> ColumnLayout with 3 rectangles
We'll be re-using my [Traffic Light QML][8], that I used in my earlier article
describing the different ways of exposing C++ classes to Qml. The Traffic
Light control is in the `GridLayout`, within a `Repeater`, 16 instances.
(The example works just as well with 500 instances). Each traffic light has a
border around it to help visualize the flow and positioning and there is a
row and column counter at the top. As a fun bonus I added a `Timer {}` with a
random interval between 2 and 15 seconds per traffic light to cycle the
different lamps. Here is how it looks, but you've already seen that in a
recording at the top of this page.
![screenshot][9]
All the source code for this example project [can be found on my github here][16].
I'm using Qt 5.15 so you can match that up if you tag along with the guide.
I've also compiled this demo to WebAssembly [here][17].
### Responsive GridLayout
Automatically resizing the `GridLayout` based on the window size is done by
specifying a bit of JavaScript code in the `columns:` and `rows:` properties
of your `GridLayout`:
readonly property int elementWidth: 150
columns: Math.max(Math.floor(parent.width / elementWidth), 1)
rows: Math.max(Math.ceil(children.length / columns), 1)
Here is how it looks inside an entire `GridLayout` contol:
GridLayout{
id: exampleLayout
readonly property int elementWidth: 150
columns: Math.max(Math.floor(parent.width / elementWidth), 1)
rows: Math.max(Math.ceil(children.length / columns), 1)
anchors.fill: parent
rowSpacing: 5
columnSpacing: rowSpacing
Repeater{
id: model
model: 16
Rectangle {
width: exampleLayout.elementWidth
height: 250
border.color: "pink"
Layout.alignment : Qt.AlignLeft | Qt.AlignTop
}
}
}
I've defined a property `elementWidth` to make sure the formula is correct. It calculates
how many columns there should be based on the width of the parent (which is the width of the
`GridLayout` due to `anchors.fill: parent`) and the width of each element.
The amount of rows is calculated based on the amount of columns and how many children there
are. I'm using the implicit property `children.length` for that, so even if you dynamically
place new items in the layout, it will still resize properly.
The `Math.max` safeguard is required so we have at least one row and one column at all times.
I had crashes when I omitted it:
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
Due to property bindings and implicit change signals the values in `rows` and `columns` are
automatically updated on each window resize. In the next paragraph I'll go in to more detail
how that all works.
You don't explicitly need to set the amount of `rows:` but because I want to show that
number in a `Text{}` I did set explicitly. Otherwise it would be `-1`.
#### Implicit change signals for every QML property
How does this work? How does the `GridLayout` knows when the window is
resized? QML has built-in property change signals (**for each property**) that are emitted whenever a
property value changes. Since width and height are properties of a control,
when they change, a signal is emitted, `widthChanged`, which you can hook up
to an `onWidthChanged:` signal handler. The ins and outs are [documented here][10]
and you can see it in action for yourself by adding a signal handler to your root `Window` control
and to your `GridLayout` or `ScrollView` control:
onWidthChanged: { console.log("Window Width changed: " + width) }
onHeightChanged: { console.log("Window Height changed: " + height)}
Here's how that looks in the example application when the window is resized:
![screenshot logging][11]
The `GridLayout` or `ScrollView` width and height are coupled to their parents (thus the `Window`)
in our example. When those parent properties change, their own properties change as well, including
each other property that uses such a value. The mechanics of property binding are [documented here][12],
I'm quoting the relevant part below:
When a property's dependencies change in value, the property is
automatically updated according to the specified relationship.
Behind the scenes, the QML engine monitors the property's dependencies
(that is, the variables in the binding expression). When a change is
detected, the QML engine re-evaluates the binding expression and applies
the new result to the property.
Property binding and re-evaluation is extremely useful but if you have a property that
is used all over the place, stuff can get messy quickly.
### Scrolling, scrollbars and a ScrollView
In the introduction I also promised to show you how to add scrollbars. If we
have too much content to fit in the window, even when the `GridLayout`
automatically resizes, scrollbars are required for the user to navigate. A
Qml `Window` does not automatically have scrollbars, you have to add them by
specifying an explicit `ScrollView` and adding your items inside of that.
You can have a scrollbar for your entire `Window` but you can also add a
`ScrollView` for certain elements only. Like a text field or an image viewer,
if something doesn't fit inside the dimensions of the element, the user can
scroll to still see everything.
This is an example of a `ScrollView`, in my example code that houses the `GridLayout`:
ScrollView {
id: scroller
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: 5
anchors.topMargin: 5
width: parent.width
height: parent.height * 0.8
clip : true
GridLayout{
...
}
}
Here is a screenshot of the example application with a GridLayout without the
`rows:` or `columns:` property set. It results in 1 row, unlimited columns:
![rowlayout][4]
In the screenshot you see a horizontal scrollbar at the bottom. If that would
not be there, only the controls on screen would be visible and usable by the
user. If they have a small screen, they might not be able to use all items
inside the layout.
If an amount of `columns:` is specified, there will be no more than that
amount of columns, but unlimited rows. Here's how a property of `columns: 2`
looks:
![columnlayout][5]
With a `ScrollView` you don't have to specify if you want a horizontal and/or
vertical scrollbar, based on the contents and dimensions of the `ScrollView`
the QML engine decides which one (or both) is required.
Depending on the window manager theme and preferences the user has
set, the scrollbars will be hidden by default until they mouse over them.
Doesn't help usability wise, but there are two properties you can set in the
`ScrollView` to control the visibility:
ScrollBar.horizontal.policy: ScrollBar.AlwaysOn
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
More information on those two properties and how they work when used
with touch gestures instead of a mouse can be found [here][12].
### Does this work on Mobile?
I've compiled this project for Android and tested it, when rotating the phone the
amount of rows and columns changes and the scrolling works as you would expect.
Nothing special had to be done except for installing the correct Android SDK and tools,
which all can be done from Qt Creator. Plugged in one of my older Android phones and
like magic, the application popped up. Below are screenshots and a screen recording.
![landscape][14]
> This is a screenshot in Landscape mode
![portrait][13]
> This is a screenshot in Portrait mode
Notice how the amount of columns and rows changes per aspect?
Here is [a video screen recording][15] showing how the application
runs on the phone.
### WebAssembly demo
For fun I compiled the example application to webassembly. Run it [here][17]
or, if it loads, an `iframe` below: