UI Styling

Info

This material is work in progress and will change!

A style changes the visual appearance of styled controls. These can be common controls as part of the component library but also application controls or even containers. To be able to style a UI successfully, all controls need to adhere to a common style programming guide which shows the developer how to ensure that newly created components are styleable.

In case the application does not use custom controls and all styling is already done by the common controls library and no work is needed here. This chapter is more for people who need to create custom controls or have specific UI requirements deviating from a common set.

Skin / Style / Theme

There are often different terms used for a similar set of functionality offered to change the appearance of the user interface. Here are the terms used in this book.

  • Skin - A skin is a re-programming of the topmost UI layer. It may re-use or adapt larger parts of the existing UI code but in general, a skin deviates so much from the original UI that it can not be embedded into the original code and often this leads to a fork of the UI layer.
  • Style - A central visual appearance hub to manage the appearance of controls. This can contain geometry, colors, effects, font and font geometry or other output methods.
  • Theme - Themes are variants of a given Style. They change some properties of it, such as the color palette, but still share the same essence and major design features. A classic example is having a "light" and a "dark" theme for you Style, which you change at runtime depending on the time of day or user preference.

A theme is the simplest visual change. A theme requires a style which supports the theming functionality. A skin changes the layout of the user interface. If the information architecture is preserved the work required is to re-write the UI layer based custom style. All appearance changes require they run on top of the same platform.

Styling versus creating new controls

You should strive to have the UI code of your QML application look familiar to any experienced QML UI developer. This means avoiding the introduction of new idioms and concepts unless when actually necessary. Qt Quick Controls 2 comes already with several common controls, such as Button, CheckBox, ComboBox, etc. It's worth making an effort to use those and make them look and behave according to your design specs via styling instead of jumping into the creation new controls such as MySpecialButton, MyProjectNameComboBox, and so on. It's a harder route, but it has multiple benefits, such as:

  • It reduces the learning curve for new members in your team, as there are less new APIs and concepts to be learned.
  • Makes your application easier to maintain. A consequence of the previous point.
  • It's easier to port QML applications made for other platforms into your platform, as there are less APIs and concepts exclusive to your project or platform, which translates into less code changes.
  • Don't reinvent the wheel, or "stand on the shoulders of giants". Time and effort was put into making the APIs of Qt Quick Controls 2 components, so it makes sense to try to use them instead of coming up with your own, which could be just duplicating effort.

Customizing an existing Style

This is the process of modifying one of the built-in styles. It is often the initial step when defining your own style. The API of the controls does not change and for an initial UI, using an existing style and tweaking it for your own purposes is notably simpler than creating a brand new one from scratch. How to customize an existing style is covered :qt5:here <qtquickcontrols2-styles.html>.

Creating a new Style

If your HMI has its own particlar look and feel, its own UI design guide, the :qt5:existing Qt Quick Controls 2 styles <qtquickcontrols2-styles.html> probably won't suite you and you will have to create your own style to implement what the UI/UX designers envisioned.

To exemplify how this is achieved let's start with a simple UI, a sort of components gallery showcasing a handful of Qt Quick Controls 2 components in different states.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import QtQuick 2.11
import QtQuick.Controls 2.1

Pane {
    width: 600
    height: 600

    Column {
        anchors.fill: parent

        padding: 50
        spacing: 25

        Row {
            spacing: 25
            Button { text: "Button" }
            Button { text: "Button"; enabled: false }
        }

        Row {
            spacing: 25
            CheckBox { text: "Checkbox" }
            CheckBox { text: "Checkbox"; checked: true }
        }

        Row {
            spacing: 25
            Switch {}
            Switch { checked: true }
        }

        Label { text: "Label" }
    }
}

Save it as, say, controls.qml. Now run it with the qml tool

1
$ qml controls.qml

You should see something like this:

Then try it with one of the styles shipped with Qt Quick Controls 2, such as "material"

1
$ qml controls.qml -style material

Or "fusion" (in a desktop environment that's using a dark theme)

1
$ qml controls.qml -style fusion

You can see that by using Qt Quick Controls 2 styles you can have the application code independent of the look and feel of its components. So no changes are needed in the components' API exposed to application code.

Now we are going to create a new style that implements a different look and feel, which we will call "foobar". The first component we will customize in the foobar style is going to be the Pane, as it's the simplest. Create a subdirectory called "foobar". Then copy the file QT_INSTALL_DIR/qml/QtQuick/Controls.2/Pane.qml to it, where QT_INSTALL_DIR is the path where your Qt is installed. It should look like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.impl 2.5
import QtQuick.Templates 2.5 as T

T.Pane {
    id: control

    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
                            contentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             contentHeight + topPadding + bottomPadding)

    padding: 12

    background: Rectangle {
        color: control.palette.window
    }
}

The idea is to collect the standard implementation of the component whose look & feel you want to customize in your style from that directory in Qt to your own style directory and then modify it at will. In our foobar style we want the Pane background to be a Gradient instead, hence we will make the following change:

1
2
3
4
5
6
7
8
    ...
    background: Rectangle {
        gradient: Gradient {
            GradientStop { position: 0.0; color: "dodgerblue" }
            GradientStop { position: 1.0; color: "lightsteelblue" }
        }
    }
    ...

Now let's run our controls.qml app with our brand new style. For that we will have to supply two additional environment variables: QT_QUICK_CONTROLS_STYLE_PATH to tell Qt where in the filesystem to look for more styles and QT_QUICK_CONTROLS_FALLBACK_STYLE to tell Qt which style to fallback on in case the chosen one is missing the implementation of some component (more info :qt5:here <qtquickcontrols2-environment.html>). Since our foobar style just has the implementation of Pane, all other components will fallback to another implementation.

1
$ QT_QUICK_CONTROLS_STYLE_PATH=. QT_QUICK_CONTROLS_FALLBACK_STYLE=Material qml controls.qml -style foobar

Next we want to style the Button component. As with did with Pane, just copy Button.qml over from QT_INSTALL_DIR/qml/QtQuick/Controls.2 into our foobar style directory. If you open that file now you will see that it's quite more involded than the Pane:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import QtQuick 2.12
import QtQuick.Controls 2.5
import QtQuick.Controls.impl 2.5
import QtQuick.Templates 2.5 as T

T.Button {
    id: control

    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
                            implicitContentWidth + leftPadding + rightPadding)
    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
                             implicitContentHeight + topPadding + bottomPadding)

    padding: 6
    horizontalPadding: padding + 2
    spacing: 6

    icon.width: 24
    icon.height: 24
    icon.color: control.checked || control.highlighted ? control.palette.brightText :
                control.flat && !control.down ? (control.visualFocus ? control.palette.highlight : control.palette.windowText) : control.palette.buttonText

    contentItem: IconLabel {
        spacing: control.spacing
        mirrored: control.mirrored
        display: control.display

        icon: control.icon
        text: control.text
        font: control.font
        color: control.checked || control.highlighted ? control.palette.brightText :
               control.flat && !control.down ? (control.visualFocus ? control.palette.highlight : control.palette.windowText) : control.palette.buttonText
    }

    background: Rectangle {
        implicitWidth: 100
        implicitHeight: 40
        visible: !control.flat || control.down || control.checked || control.highlighted
        color: Color.blend(control.checked || control.highlighted ? control.palette.dark : control.palette.button,
                                                                    control.palette.mid, control.down ? 0.5 : 0.0)
        border.color: control.palette.highlight
        border.width: control.visualFocus ? 2 : 0
    }
}

You're free to change anything at will. This is just a default look & feel implementation. You're free to take it as it is, do some small modifications on top of it or wipe it out and do something completely different. What's important is to try to and obey the exising properties (icon, text, impleicitWidth, etc) as much as it makes sense to in your HMI usage and to put the foreground content of your button (eg, text and icon) in contentItem and its background, if any, in background.

It's worth noting the widespread usage of the palette property. If you want to tweak its values in your style, the best place to do so would be in the Control.qml style implementation, as all Qt Quick Controls 2 components inherit from it. But if the categories (button, windowText, highlight, etc) in that palette type don't really suite your needs or your UI design guide you're free to use your own structure to keep your custom color and other values instead. We will come back to it later.

For now let's just set a hardcoded background color, make the background rounded, make the button larger when down/pushed and have its text rotating (just because we can :) ). These would be the modifications:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
--- a/foobar/Button.qml
+++ b/foobar/Button.qml
@@ -15,12 +15,18 @@ T.Button {
     horizontalPadding: padding + 2
     spacing: 6

+    scale: control.down ? 1.4 : 1
+    Behavior on scale {
+        NumberAnimation { easing.type: Easing.OutCubic; duration: 200 }
+    }
+
     icon.width: 24
     icon.height: 24
     icon.color: control.checked || control.highlighted ? control.palette.brightText :
                 control.flat && !control.down ? (control.visualFocus ? control.palette.highlight : control.palette.windowText) : control.palette.buttonText

     contentItem: IconLabel {
+        id: iconLabel
         spacing: control.spacing
         mirrored: control.mirrored
         display: control.display
@@ -30,15 +36,29 @@ T.Button {
         font: control.font
         color: control.checked || control.highlighted ? control.palette.brightText :
                control.flat && !control.down ? (control.visualFocus ? control.palette.highlight : control.palette.windowText) : control.palette.buttonText
+
+        RotationAnimator {
+            target: iconLabel
+            from: 0; to: 360
+            duration: 1500
+            running: true
+            loops: Animation.Infinite
+        }
     }

     background: Rectangle {
         implicitWidth: 100
         implicitHeight: 40
         visible: !control.flat || control.down || control.checked || control.highlighted
-        color: Color.blend(control.checked || control.highlighted ? control.palette.dark : control.palette.button,
-                                                                    control.palette.mid, control.down ? 0.5 : 0.0)
+
+        color: "seagreen"
+        opacity: control.down ? 1 : 0.8
+        Behavior on opacity {
+            NumberAnimation { easing.type: Easing.OutCubic; duration: 200 }
+        }
+
         border.color: control.palette.highlight
         border.width: control.visualFocus ? 2 : 0
+        radius: width / 2
     }
 }

If you run that application again you should see that the buttons are animated and look wildly different from the other styles. This is just to give an idea of how flexible and powerful the Qt Quick Controls 2 styling is.

Collecting values in a Style object

So far we have been hardcoding color values directly in the component's style implementation. But for better reusability it's prefferable to give them names and collect them all into a single entity. There are a couple of ways of doing it but, again, we will start with the simplest: creating a new qml module containing a singleton QtObject which will hold all the color and other values used throughout the style implementation. In this example we will name that singleton FoobarStyle. Create a subdirectory called imports and inside it yet another subdirectory called FoobarStyle, which will be the name of our qml module.

Inside imports/FoobarStyle create a file named qmldir with the following content::

1
2
module FoobarStyle
singleton FoobarStyle 1.0 FoobarStyle.qml

Then proceed to create the file FoobarStyle.qml also inside imports/FoobarStyle::

1
2
3
4
5
6
7
8
9
pragma singleton

import QtQuick 2.11

QtObject {
    property color gradientBackgroundTopColor: "dodgerblue"
    property color gradientBackgroundBottomColor: "lightsteelblue"
    property color buttonBackgroundColor: "seagreen"
}

FoobarStyle collects the colors have been used so far. You can have those names be more specific (eg. button background color) or more generic (eg. secondary control color) according to how they're used throught the implenentation and how your UI Style guide describes them. Now let's get back to Panel.qml and Button.qml replacing the hardcoded values with their corresponding named colors::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
--- a/foobar/Button.qml
+++ b/foobar/Button.qml
@@ -3,6 +3,8 @@ import QtQuick.Controls 2.5
 import QtQuick.Controls.impl 2.5
 import QtQuick.Templates 2.5 as T

+import FoobarStyle 1.0
+
 T.Button {
     id: control

@@ -51,7 +53,7 @@ T.Button {
         implicitHeight: 40
         visible: !control.flat || control.down || control.checked || control.highlighted

-        color: "seagreen"
+        color: FoobarStyle.buttonBackgroundColor
         opacity: control.down ? 1 : 0.8
         Behavior on opacity {
             NumberAnimation { easing.type: Easing.OutCubic; duration: 200 }
diff --git a/foobar/Pane.qml b/foobar/Pane.qml
index f903afa..b82be7f 100644
--- a/foobar/Pane.qml
+++ b/foobar/Pane.qml
@@ -3,6 +3,8 @@ import QtQuick.Controls 2.5
 import QtQuick.Controls.impl 2.5
 import QtQuick.Templates 2.5 as T

+import FoobarStyle 1.0
+
 T.Pane {
     id: control

@@ -15,8 +17,8 @@ T.Pane {

     background: Rectangle {
         gradient: Gradient {
-            GradientStop { position: 0.0; color: "dodgerblue" }
-            GradientStop { position: 1.0; color: "lightsteelblue" }
+            GradientStop { position: 0.0; color: FoobarStyle.gradientBackgroundTopColor }
+            GradientStop { position: 1.0; color: FoobarStyle.gradientBackgroundBottomColor }
         }
     }
 }

Now to be able to run our controls.qml application we will also have to tell the qml tool to look into the imports subdirectory for qml modules. Hence::

1
QT_QUICK_CONTROLS_STYLE_PATH=. QT_QUICK_CONTROLS_FALLBACK_STYLE=Material qml -I imports controls.qml -style foobar

Note that having our named colors conveniently collected in a singleton also enables application code import FoobarStyle and use them directly whenever needed.

Adding theming support

After having our hardcoded values such as named colors collected in a singleton, adding theming support is a simple, straightforward, step. The idea is that a single property in FooBarStyle, which we will name as theme, will define the value of all others. So this is our improved FoobarStyle.qml, now with theme support:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
pragma Singleton

import QtQuick 2.11

QtObject {
    // Available themes
    readonly property int dayTheme: 0
    readonly property int nightTheme: 1

    // The chosen theme
    property int theme: dayTheme

    // The values that make up a theme
    readonly property color gradientBackgroundTopColor: theme === dayTheme ? "dodgerblue" : "black"
    readonly property color gradientBackgroundBottomColor: theme === dayTheme ? "lightsteelblue" : "darkblue"
    readonly property color buttonBackgroundColor: theme === dayTheme ? "seagreen" : "maroon"
}

And modifying the application code, controls.qml, so that clicking on the first button switches the theme::

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
--- a/controls.qml
+++ b/controls.qml
@@ -1,6 +1,8 @@
 import QtQuick 2.11
 import QtQuick.Controls 2.1

+import FoobarStyle 1.0
+
 Pane {
     width: 500
     height: 400
@@ -13,7 +15,16 @@ Pane {

         Row {
             spacing: 25
-            Button { text: "Button" }
+            Button {
+                text: FoobarStyle.theme === FoobarStyle.dayTheme ? "Day" : "Night"
+                onClicked: {
+                    if (FoobarStyle.theme === FoobarStyle.dayTheme) {
+                        FoobarStyle.theme = FoobarStyle.nightTheme;
+                    } else {
+                        FoobarStyle.theme = FoobarStyle.dayTheme;
+                    }
+                }
+            }
             Button { text: "Button"; enabled: false }
         }

This is how our controls.qml application should look like after having the theme support added to it:

Naming conventions

One common mistake when naming colors in particular is naming them after what they look like instead of their function or where they are used. So avoid names such as "orangeBackgroundColor" or "lightTextColor" and prefer usage, such as "buttonBackgroundColor" or a category such as "primaryColor" or "accentColor". After all, as soon as you have a day/light and a night/dark theme, a color property that used to have a "yellow" or "light" value will switch to have the opposite, having a "dark" or "brown" one instead. Naming a color property after the visual caracteristics of its value will void the abstraction level (and hence flexibility) it provides when compared to using hardcoded values directly.

Having that said, there might still be value in naming a color in a explicit way in your style object, such as FoobarStyle.red, meaning that whenever your UI uses red, it's not just any red, or the standard 0xFF0000, but a very particular hue of red, which is specified in your style. And that only if the usage of this named red throught the UI is always the same, regardless of the theme. Such as the red that makes up the visual identity of your company or the red that signifies alert or the interruption of a call.

Note

describe why a sheet is a good concept to display your controls and how should it be built.