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.
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:
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:
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.
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:
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.
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:
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.