UI Dynamics and Dynamic UI

In the previous sections, we learned how to add items while developing an application and make them invisible when needed. What should we do if we’d like our applications to look totally different depending on how the data and user input changes? These changes might be aimed toward modifying more then just visibility. This might be quite complex with Qt Quick as we need to change all related elements. How do we make dynamic changes in the application UI visually appealing or even make them a part of the user experience? We have not covered this at all.

Using States

Access to the network is essential for the weather related part of our application in the current version. It visualizes data received from the internet. If your computer is offline and you start the clock-n-weather application in qt_quick_app_dev_intro_src.zip (see Downloads section), you will see just the clock and a lot of empty space around it:

../_images/clock_n_weather_offline.png

This is because WeatherModelItem failed to get the weather data. Due to this, there are no model items to display. If you use this application on a notebook or on a mobile device, this situation might occur very frequently. It would be great if our application would be able to handle situations when the network is down. We can accomplish this by using the State [http://qt-project.org/doc/qt-4.8/qml-state.html] item provided by Qt Quick.

Each item in Qt Quick has a state property which holds the name of the current state. There is also a states property which is a list of States [http://qt-project.org/doc/qt-4.8/qml-state.html]. This property contains all states known for that item. Each of the States [http://qt-project.org/doc/qt-4.8/qml-state.html] in the list has a string name and defines a set of property values. If required, it can even contain some script code, which is executed when that state becomes the current one. An item can be set to a state just by assigning the name of a selected state to the state property. See the documentation for State [http://qt-project.org/doc/qt-4.8/qml-state.html] and QML States [http://qt-project.org/doc/qt-4.8/qdeclarativestates.html] for more details.

We will add three states to the main item of our application:

  • Offline* - It is an initial state in the startup phase. It is also applied if there is no network connection or the if application should stay offline
  • Loading* - A network connection is available, but WeatherModelItem is still loading weather data. This state is useful on slow network connections (on mobile devices for example).
  • Live Weather* - Updated weather data is available and displayed.

In the Offline* and Loading states, the application should show just the clock in a larger size in the middle of the screen. When Live Weather is active, the application should show the weather data as well.

As our new states are so closely related to the status of the WeatherModelItem, we just bind them directly. The WeatherModelItem does not define any real states. We hijack its states property to store Offline, Loading and Live Weather values depending on the status of the current or forecast models:

(src/components/WeatherModelItem.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1

Item {
    id: root
    property alias forecastModel: forecast
    property alias currentModel: current
    property string location: "Munich"
    property bool forceOffline: false
    property string baseURL: "http://www.google.com"
    property string dataURL: "/ig/api?weather="
    property string source: baseURL + dataURL + location.split(' ').join('%20')
    property int interval: 5
    property bool modelDataError: false
    property string statusMessage: ""

    XmlListModel {
        id: forecast
        source: root.source
        query: "/xml_api_reply/weather/forecast_conditions"

        XmlRole { name: "day_of_week"; query: "day_of_week/@data/string()" }
        XmlRole { name: "low"; query: "low/@data/string()" }
        XmlRole { name: "high"; query: "high/@data/string()" }
        XmlRole { name: "condition"; query: "condition/@data/string()" }
        XmlRole { name: "temp_c"; query: "temp_c/@data/string()" }

        onStatusChanged: {
            root.modelDataError = false
            if (status == XmlListModel.Error) {
                root.state = "Offline"
                root.statusMessage = "Error occurred: " + errorString()
                root.modelDataError = true
                //console.log("Weather Clock: " + root.statusMessage)
            } else if (status == XmlListModel.Ready) {
                // check if the loaded model is not empty, and post a message
                if (get(0) === undefined) {
                    root.state = "Offline"
                    root.statusMessage = "Invalid location \"" + root.location + "\""
                    root.modelDataError = true
                } else {
                    root.state = "Live Weather"
                    root.statusMessage = "Live current weather is available"
                }
                //console.log("Weather Clock: " + root.statusMessage)
            } else if (status == XmlListModel.Loading) {
                root.state = "Loading"
                root.statusMessage = "Forecast data is loading..."
                //console.log("Weather Clock: " + root.statusMessage)
            } else if (status == XmlListModel.Null) {
                root.state = "Loading"
                root.statusMessage = "Forecast data is empty..."
                //console.log("Weather Clock: " + root.statusMessage)
            } else {
                root.modelDataError = false
                console.log("Weather Clock: unknown XmlListModel status:" + status)
            }
        }
    }

    XmlListModel {
        id: current
        source: root.source
        query: "/xml_api_reply/weather/current_conditions"

        XmlRole { name: "condition"; query: "condition/@data/string()" }
        XmlRole { name: "temp_c"; query: "temp_c/@data/string()" }

        onStatusChanged: {
            root.modelDataError = false
            if (status == XmlListModel.Error) {
                root.state = "Offline"
                root.statusMessage = "Error occurred: " + errorString()
                root.modelDataError = true
                //console.log("Weather Clock: Error reading current: " + root.statusMessage)
            } else if (status == XmlListModel.Ready) {
                // check if the loaded model is not empty, and post a message
                if (get(0) === undefined) {
                    root.state = "Offline"
                    root.statusMessage = "Invalid location \"" + root.location + "\""
                    root.modelDataError = true
                } else {
                    root.state = "Live Weather"
                    root.statusMessage = "Live current weather is available"
                }
                //console.log("Weather Clock: " + root.statusMessage)
            } else if (status == XmlListModel.Loading) {
                root.state = "Loading"
                root.statusMessage = "Current weather is loading..."
                //console.log("Weather Clock: " + root.statusMessage)
            } else if (status == XmlListModel.Null) {
                root.state = "Loading"
                root.statusMessage = "Current weather is empty..."
                //console.log("Weather Clock: " + root.statusMessage)
            } else {
                root.modelDataError = true
                console.log("Weather Clock: unknown XmlListModel status:" + status)
            }
        }
    }

    Timer {
        // note that this interval is not accurate to a second on a full minute
        // since we omit adjustment on seconds like in the clock interval
        // to simplify the code
        interval: root.interval*60000
        running: Qt.application.active && !root.forceOffline
        repeat: true
        onTriggered: {
            current.reload()
            forecast.reload()
        }
    }
}

The actual states are introduced in the main item, WeatherClock. This item gets two new child items holding all elements to be displayed in states with different visualization:

  • clockScreen item - shows a bigger clock when the main item is in Offline or Loading state
  • weatherScreen item - shows clock and the weather forecast during the Live Weather state, which is basically the same as we had in the clock-n-weather application.

As a final step, we just bind the states of WeatherClock to the values of the WeatherModelItem state:

...
Rectangle {
    id: root
    ...
    state: forceOffline ? "Offline" : weatherModelItem.state
    ...
    states: [
        State {
            name: "Offline"
            PropertyChanges {target: clockScreen; visible: true}
            PropertyChanges {target: weatherScreen; visible: false}
        },
        State {
            name: "Live Weather"
            PropertyChanges {target: clockScreen; visible: false}
            PropertyChanges {target: weatherScreen; visible: true}
        },
        State {
            name: "Loading"
            PropertyChanges {target: clockScreen; visible: true}
            PropertyChanges {target: weatherScreen; visible: false}
            PropertyChanges {target: busyIndicator; on: true}
        }
    ]
...
}

Our State [http://qt-project.org/doc/qt-4.8/qml-state.html] definitions contain PropertyChanges [http://qt-project.org/doc/qt-4.8/qml-propertychanges.html] items which change the visibility of our new screens and turn on the busyIndicator in the Loading* state.

The Loading state might be active for quite some time. If the clock does not show seconds, the whole application might appear as if it were hanging. We need a animated busy indicator to show the user that the application is still running. The Qt example RSS News Reader [http://qt-project.org/doc/qt-4.8/demos-declarative-rssnews-qml-rssnews-content-rssfeeds-qml.html] provides a very nice one. We can use that with minor modifications. Our busyIndicator becomes visible in the Loading state and informs the user that the application is processing data in the background.

You may have noticed that we use the new forceOffline setting here, which was first spotted in the last chapter. If forceOffline is set to true, the application stays in the Offline state regardless of changes in weatherModelItem.

If we now change states, changes occur instantly. The application would look more attractive if there were transitions and animation effects applied during state changes. We will take a look at this in the next section.

Adding Animations

Animations are not only useful for visual effects. They can also serve as a base for features that could be difficult to get done by other means (for example, our busy indicator mentioned in the last section). Qt Quick provides a very rich animation framework that is simple to use. Covering it in great detail is beyond the scope of this guide, but we can spend some time understanding what animations do and how to start using them.

Generally, all animations manipulate one or more properties of an element, thereby modifying its visual appearance. This modification can have various dynamics and run in various time spans. There can be numerous animations running in parallel or sequentially applied to the same or to different elements. You can start an animation explicitly or implicitly upon a property change. You can also permanently assign an animation to a property so that an animation starts as soon as a property changes. Although there is a generic Animation [http://qt-project.org/doc/qt-4.8/qml-animation.html] element, most of the time, you will probably use one of the predefined animation elements [http://qt-project.org/doc/qt-4.8/qml-animation-transition.html] provided by Qt Quick. It’s very easy to add animations to an application. The major challenge is to find out which animations to use and how to use them to compose the required visual effect.

Animations are very related to Transitions [http://qt-project.org/doc/qt-4.8/qml-transition.html], which defines how an element is transformed from one State [http://qt-project.org/doc/qt-4.8/qml-state.html] to another. In most cases, a transition includes an animation.

Qt documentation provides an overview of all animations and transitions, and provides details about using them in the QML Animation and Transitions [http://qt-project.org/doc/qt-4.8/qdeclarativeanimation.html] article.

The code segment below shows two transitions between the Offline and Live Weather states in our application:

transitions: [
    Transition {
        from: "Offline"
        to: "Live Weather"
        PropertyAnimation {
            target: weatherScreen
            property: "opacity"
            from: 0
            to: 1
            easing.type: Easing.Linear
            duration: 5000
        }
    },
    Transition {
        from: "Live Weather"
        to: "Offline"
        PropertyAnimation {
            target: clockScreen
            property: "opacity"
            from: 0
            to: 1
            easing.type: Easing.Linear
            duration: 5000
        }
    }
]

The state changes swap the visibility of the off-line view and the full view with weather data. On top of this, we add an animation which changes the opacity property. This fades the screen out letting it disappear fully in 5 seconds.

Note

Theoretically, a slight flickering might be visible on the screen in the beginning of transitions as the target element becomes fully visible first and immediately after this its opacity is turned to 0 in the beginning of the animation.

The functionality of our busy indicator is completely based on animations! There is almost no other code in its implementation:

(utils/BusyIndicator.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

// This is taken from the "RSS News" demo provided in Qt
// The original code has been modified to adapt to the application structure

import QtQuick 1.1

Image {
    id: root
    property bool on: false

    source: "../content/resources/busy.png"
    visible: root.on

    NumberAnimation on rotation {
        running: root.on; from: 0; to: 360; loops: Animation.Infinite; duration: 1200
    }
}

We load BusyIndicator as follows:

// it is off and invisible by default
BusyIndicator {
    id: busyIndicator
    anchors.horizontalCenter: root.horizontalCenter
    anchors.bottom: statusText.top
    anchors.margins: 10
}

And this is how it looks in our application when it starts up:

../_images/busy_indicator.png

Another animation is used to implement a visual effect on the clock and weather items in the reworked main item of our application. This is discussed in the next section.

Supporting the Landscape Mode

If our application is to run on a mobile device, it should have a layout of the clockScreen and weatherScreen tailored to the landscape display orientation. We do not need many changes in clockScreen for this, as it contains only one item. Changes in weatherScreen might be larger...

An interesting approach toward simplifying the implementation is to use Flow [http://qt-project.org/doc/qt-4.8/qml-flow.html] instead of the previously used Column [http://qt-project.org/doc/qt-4.8/qml-column.html]. Flow [http://qt-project.org/doc/qt-4.8/qml-flow.html] arranges its children dynamically depending on its own size. If needed, it wraps children into the appropriate rows and columns.

Flow [http://qt-project.org/doc/qt-4.8/qml-flow.html] has one more cool feature. This is the move property where we can define a Transition [http://qt-project.org/doc/qt-4.8/qml-transition.html], which is applied when the children in a Flow [http://qt-project.org/doc/qt-4.8/qml-flow.html] start moving. We use a NumberAnimation [http://qt-project.org/doc/qt-4.8/qml-numberanimation.html] applied to the coordinates of the children and select a bounce effect (Easing.OutBounce) for easing.type:

...
move: Transition {
    NumberAnimation {
        properties: "x,y"
        duration: 500
        easing.type: Easing.OutBounce
    }
}
...

This is how our application looks on the screen if we resize the main window:

../_images/weather_app_resized.png

Finalizing the Main Item

We need to rework on the main item to add a few new features. You’ve seen parts of the related code in this and in the earlier sections. Let’s put them all together and take a look at some other details.

First, we take the code from the main item, ClockAndWeather.qml (see clock-n-weather/ClockAndWeather.qml in qt_quick_app_dev_intro_src.zip, see Downloads section) and add animations and transitions as discussed in this chapter.

Additionally, the reworked main item gets three buttons and a status text at the bottom of the screen.

Clicking this exitButton is now used to quit the application. Clicks inside the root items are not used for this anymore.

The toggleStatesButton allows the user to force the Offline state. This is useful to use the screen space for a bigger clock by hiding the weather forecast. It prevents regular data transfer over Internet as well.

The configureButton displays the the configure element, which holds and manipulates the configuration parameters. The main item just binds them to the appropriate properties of other items. This implements a kind of global application state. We will discuss alternative solutions for this in the last chapter.

The status text is updated upon changes to the states.

The complete code of the new main item looks like this:

(WeatherClock/WeatherClock.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)

import QtQuick 1.1

import "../utils" 1.0
import "../components" 1.0
import "../js/style.js" as Style
import "../js/logic.js" as Logic

Rectangle {
    id: root
    property string defaultLocation: configure.locationText
    property int defaultInterval: configure.forecastUpdateInterval
    property bool showSeconds: configure.showSeconds
    property bool showDate: configure.showDate
    property bool forceOffline: configure.forceOffline
    state: forceOffline ? "Offline" : weatherModelItem.state

    width: 360
    height: 640

    onStateChanged: {
        if (state == "Offline")
            statusText.showStatus ("offline");
        else if (state == "Loading")
            statusText.showStatus ("loading...");
        else if (state == "Live Weather")
            statusText.showStatus ("live weather");
    }

    Image {
        id: background
        source: Style.backgroundImage
        fillMode: "Tile"
        anchors.fill: parent
        onStatusChanged: if (background.status == Image.Error)
                                console.log("Background image \"" +
                                            source +
                                            "\" cannot be loaded")
    }

    Dialog {
        id: errorDialog
        width: root.width
        anchors.centerIn: parent
        z: root.z+1
        visible: false
    }

    WeatherModelItem {
        id: weatherModelItem
        location: root.defaultLocation
        interval: root.defaultInterval
        forceOffline: root.forceOffline

        onModelDataErrorChanged: {
            if (weatherModelItem.modelDataError)
                errorDialog.show(weatherModelItem.statusMessage)
        }
    }

    Component {
        id: weatherCurrentDelegate
        Weather {
            id: currentWeatherItem
            labelText: root.defaultLocation
            conditionText: model.condition
            tempText: model.temp_c + "C°"
        }
    }

    Component {
        id: weatherForecastDelegate
        Weather {
            id: forecastWeatherItem
            labelText: model.day_of_week
            conditionText: model.condition
            tempText: Logic.f2C (model.high) +
                      "C° / " +
                      Logic.f2C (model.low) +
                      "C°"
        }
    }

    NightClock {
        id: clockScreen
        height: 130
        anchors.centerIn: root
        showDate: root.showDate
        showSeconds: root.showSeconds
        textColor: Style.offlineClockTextColor
    }

    Flow {
        id: weatherScreen
        width: root.width
        height: root.height
        anchors.fill: parent
        anchors.margins: Style.baseMargin
        spacing: 30

        NightClock {
            id: clock
            height: 80
            width: 190
            showDate: root.showDate
            showSeconds: root.showSeconds
            textColor: Style.onlineClockTextColor
        }

        ListView {
            id: currentWeatherView
            width: 100
            height: 100
            model: weatherModelItem.currentModel
            delegate: weatherCurrentDelegate
            interactive: false
        }

        Repeater {
            id: forecastWeatherView
            model: weatherModelItem.forecastModel
            delegate: weatherForecastDelegate
        }

        move: Transition {
            NumberAnimation {
                properties: "x,y"
                duration: 500
                easing.type: Easing.OutBounce
            }
        }
    }

    Text {
        id: statusText
        anchors.horizontalCenter: root.horizontalCenter
        anchors.bottom: exitButton.top
        anchors.margins: Style.baseMargin
        color: Qt.lighter(Style.penColor)
        font.pixelSize: Style.textPixelSize*0.8
        text: qsTr("Status: starting...")

        function showStatus (newStatusText) {
            text = qsTr("Status: " + newStatusText);
        }
    }

    // it is off and invisible by default
    BusyIndicator {
        id: busyIndicator
        anchors.horizontalCenter: root.horizontalCenter
        anchors.bottom: statusText.top
        anchors.margins: Style.baseMargin
    }

    Button {
        id: configureButton
        text: qsTr("Config")
        anchors.left: root.left
        anchors.bottom: root.bottom
        anchors.margins: Style.baseMargin
        onClicked: {
            configure.visible = true;
        }

    }

    Button {
        id: exitButton
        text: qsTr("Exit")
        width: configureButton.width
        anchors.right: root.right
        anchors.bottom: root.bottom
        anchors.margins: Style.baseMargin
        onClicked: Qt.quit()
    }

    Button {
        id: toggleStatesButton
        anchors.right: exitButton.left
        anchors.left: configureButton.right
        anchors.bottom: root.bottom
        anchors.margins: Style.baseMargin
        // simple binding like this "text: root.state" works here to, but it is more diifcult to translate then.
        // we use explicit strngs instead
        text: root.state == "Offline" ? qsTr("Get weather") : qsTr("Go offline")
        onClicked: {
            if (root.state == "Offline")
                configure.forceOffline = false;
            else
                configure.forceOffline = true;
        }
        // for experimental purposes...
        // onPressedAtXY: {
        //     console.log ("pressed at: " + coordinates)
        // }
    }

    Configure {
        id: configure
        anchors.fill: root
        z: root.z + 1
        visible: false
        showSeconds: true
        showDate: true
        forecastUpdateInterval: 5
        locationText: qsTr("Munich")
        forceOffline: false
    }

    states: [
        State {
            name: "Offline"
            PropertyChanges {target: clockScreen; visible: true}
            PropertyChanges {target: weatherScreen; visible: false}
        },
        State {
            name: "Live Weather"
            PropertyChanges {target: clockScreen; visible: false}
            PropertyChanges {target: weatherScreen; visible: true}
        },
        State {
            name: "Loading"
            PropertyChanges {target: clockScreen; visible: true}
            PropertyChanges {target: weatherScreen; visible: false}
            PropertyChanges {target: busyIndicator; on: true}
        }
    ]

    transitions: [
        Transition {
            from: "Offline"
            to: "Live Weather"
            PropertyAnimation {
                target: weatherScreen
                property: "opacity"
                from: 0
                to: 1
                easing.type: Easing.Linear
                duration: 5000
            }
        },
        Transition {
            from: "Live Weather"
            to: "Offline"
            PropertyAnimation {
                target: clockScreen
                property: "opacity"
                from: 0
                to: 1
                easing.type: Easing.Linear
                duration: 5000
            }
        }
    ]
}

What’s Next?

Our application is now complete and you have learned major aspects of Qt Quick!

Certainly, our final application can be enhanced and extended with many features. We selected a minimal subset to cover the scope of this guide without going into too many details. The next and the last chapter discuss a few selected enhancements.