Friday 4 July 2014

Scaling content in QML

A HsQML user e-mailed me to ask me how they could get a QML Item to fill the parent Window and I thought it would make an interesting blog post. The easy answer to that question is to use anchors to snap the boundaries of the Item to its parent's as follows:

Item {
    anchors.fill: parent;
    ...

However, in a sense, that only defers the layout problem to the Item's children. They still need to position themselves in the variable amount of space offered by their parent, either using anchors again, one of the special layout elements such as Grid or GridLayout, or by property binding each child's positioning properties (x, y, etc.) to a custom layout calculation.

Qt Quick has a number of facilities to help you to lay out your Items, but there's one particular problem that it takes a few stabs at but still doesn't make especially easy. If you have an Item which has a particular aspect ratio, how do you scale it to fill the available space while still retaining the correct aspect?

The Image element solves this problem for itself with the fillMode property, which among other things allows you to preserve the source image aspect via either fitting or cropping. However, this property is specific to images and it doesn't affect any child elements you might want to position relative to the image. It does exposes the results of its calculations via the paintedWidth and paintedHeight properties so you could use this to help you place them manually, but it would still require a little work.

On the other hand, the QQuickView C++ class offers a solution at the top-level by extending the view to support scaling QML content automatically. If HsQML used this convenience class to display its QML documents that don't have explicit Windows, it could be set to always resize the content to fit the window via the setResizeMode() method. However, this would only help to resize whole documents and then only when explicit windows weren't used.

Fortunately, it's not too difficult to build an implementation of scale-to-fit using transforms and property binding. This involves specifying both a scaling transform on the content Item to size it to fit its parent and a translating transform to centre the content if the parent has a different aspect. Any children of content Item will be positioned according to the coordinate system specified by the content Item's fixed dimensions and will be transformed along with the Item to the desired size.

The scale factor and offsets can be calculated by expressions which make use of the parent Item or Window's width and height properties. QML's property binding facility means that whenever the width or height changes, the expressions which use those values will be re-evaluated and hence the layout updated. The following example demonstrates implementing this with an explicit Window element in order to make its properties accessible for binding:

import QtQuick 2.0
import QtQuick.Window 2.0

Window {
    id: win; width: 200; height: 200;

    Rectangle {
        id: rect; width: 100; height: 100; color: 'blue';

        transform: [
            Scale {
                id: scale; xScale: yScale;
                yScale: Math.min(
                    win.width/rect.width,
                    win.height/rect.height);},
            Translate {
                x: (win.width-rect.width*scale.xScale)/2;
                y: (win.height-rect.height*scale.yScale)/2;}]

        Rectangle {
            x: 0; y: 0; width: 50; height: 50; color: 'red';
        }
    }
}

This can also be made to work when you don't have an explicit Window by accessing its properties via the parent property of the root Item. The important thing to note here is that the root's parent will be initially be null when the document loads. It's only when HsQML notices that the root element isn't a Window that it will create the implicit Window for you, so a little care has to be taken to ensure that the parent's properties aren't accessed until the parent has actually been set. I took the opportunity to implement this in the Morris demo application which didn't previously handle scaling correctly, or you can look at the example code below:

Rectangle {
    id: rect; width: 100; height: 100; color: 'blue';

    property real pw : parent!=null?parent.width:width;
    property real ph : parent!=null?parent.height:height;

    transform: [
        Scale {
            id: scale; xScale: yScale;
            yScale: Math.min(
                rect.pw/board.width,rect.ph/board.height);},
        Translate {
            x: (rect.pw-board.width*scale.xScale)/2;
            y: (rect.ph-board.height*scale.yScale)/2;}]

N.B. Nothing in this post is really specific to HsQML and you can equally try out these examples with the qmlscene tool.