Scalable UI in QtQuick

Submitted by mimec on 2014-09-09

One of the first problems that anyone developing QtQuick applications encounters is the scalability of the user interface. On Windows, when you change the size of text in the Display settings in the Control Panel, the fonts become larger or smaller, but the window and controls don't. When you run the same application on Mac OS X, the font sizes are completely different than on Windows. The situation is even more complicated on mobile devices, which have a wide variety on both screen resolutions and screen sizes - you can find both a mobile with 5" display and 1920x1080 resolution and a tablet with 10" display and 1280x800 resolution.

Web developers, who face similar problems, solve them by using relative font sizes (such as 0.8em or 1.5em) and logical pixels which are scaled to physical device pixels (according to the <meta name="viewport"> tag). Exactly the same approach can be used in QtQuick applications, but it requires some additional work. After some research and experimentation, I created the following simple component which I called Units.qml:

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Private 1.0

QtObject {
    function dp( x ) {
        return Math.round( x * Settings.dpiScaleFactor );
    }

    function em( x ) {
        return Math.round( x * TextSingleton.font.pixelSize );
    }
}

As you can see, the Units component provides two simple functions:

  • dp() - converts logical pixels to device (physical) pixels. Generally, all absolute sizes, positions, margins and other distances should always be specified using this function.
  • em() - converts relative font size to pixels. This function should be used for specifying all font sizes, but it can also be useful for some sizes and distances (e.g. the height of a button can be specified as a multiple of the font size, provided that appropriate layouts are used).

This is an example of how this component can be used:

import QtQuick 2.2
import QtQuick.Controls 1.2

Item {
    id: root

    implicitWidth: u.dp( 640 )
    implicitHeight: u.dp( 480 )

    Label {
        id: helloLabel

        x: u.dp( 20 )
        y: u.dp( 50 )

        text: "hello"
        font.pixelSize: u.em( 1.5 )
    }

    Units {
        id: u
    }
}

How does it work? The Settings object is an internal singleton of the QtQuick Controls module which provides various information. The dpiScaleFactor property basically uses the logicalDotsPerInchX property of the primary screen to calculate the scale factor. On Windows, this corresponds to the size of text in the Display settings in the Control Panel, and typical values are 1 (small) or 1.25 (normal). On Mac OS X, the dpiScaleFactor is always 1, however on Retina displays this actually corresponds to 2 physical pixels, because the OS performs additional scaling. On mobile devices you may need to use a different scale factor to ensure that the window fits the entire screen regardless of it's dimensions and DPI resolution.

Note that the dp() function uses Math.round() to ensure that the result is an integer value. Although QtQuick elements can be positioned at non-integer coordinates, this can result in ugly artifacts, so it's better to round everything.

The TextSingleton, as the name implies, is another internal singleton of the QtQuick Controls module which is simply an instance of Text component. It is used to access the pixel size of the default font. The result is also rounded in case it's used to position elements.

The Units component could also be made a singleton; in that case it wouldn't be necessary to put an instance of the Units object into every component that uses it. On the other hand, writing "u.dp()" is easier and more readable than "Units.dp()". Besides, using the singleton messes up the design mode in Qt Creator. Of course, when designing the UI, you still have to add the function calls manually and you cannot simply re-position items by dragging them in the design mode, but at least the Qt Creator displays everything correctly when the Units object is explicitly placed in the component.