The next step is to get a version that combines the features of the weather forecast application and the clock application we have already developed. Fortunately, we need not implement those features again or copy the code. We can slightly modify the available applications and re-use them as components. This will be the focus of this chapter.
In the next chapter, we will take another step forward and further enhance the application by adding more components while learning more about how they can be used.
The notion of components in Qt Quick is very simple: any item composed from other elements or components can become a component itself. Components are building blocks for any larger application. When using modules, it is possible to create component collections (libraries).
In order to create a component, you have to create a new file saved as <NameOfComponent>.qml with only one root element in it (the same as you would do with an ordinary Qt Quick application). It is important to underline that the name of the file has to start with a capital letter. From now on, the new component will be available under the name <NameOfComponent> to all other Qt Quick applications residing in the same directory. Generally, files with a qml extension are referred to as QML Documents [http://qt-project.org/doc/qt-4.8/qdeclarativedocuments.html].
When your work progresses, you will probably get many files in the application’s folder. Later on, you might even have the need to host components in different versions. This is where Qt Quick modules come to the rescue. The first step is to move all components (basically, files) which belong to the same group of functionality in a new folder. Then you need to create a qmldir file containing meta-information about these components in that folder. Afterwards this folder becomes a module, which can be imported into your application the same way as you would import the standard Qt Quick elements:
import QtQuick 1.1
import "components" 1.0
See Defining New Components [http://qt-project.org/doc/qt-4.8/qmlreusablecomponents.html#qml-define-components] in Qt documentation for more details about components.
If you move the directory with modules, you have to change the path in all QML documents using them. It is also possible to provision modules for global use by any application. See the QML Modules [http://qt-project.org/doc/qt-4.8/qdeclarativemodules.html] article for more details.
Note
Components can also be developed as C++ plug-ins, which is beyond the scope of this guide. See this article [http://qt-project.org/doc/qt-4.8/qml-extending.html] if you are interested.
In some cases, you must define in-line components, for example, when you pass a reference to a component to another element in the same QML file. This is used more frequently for delegates in views. See the main item’s code of the final application in the Integrated Application section.
If you experience problems while importing modules or separate components, set the environment variable QML_IMPORT_TRACE to 1 (see “Debugging QML” [http://qt-project.org/doc/qt-4.8/qdeclarativedebugging.html]) for more debugging tips.
Let’s take a look at how this is used in our application.
We move NightClock.qml to a new folder called components, which also contains two new components: Weather and WeatherModelItem. As mentioned above, we also add a qmldir file to describe a new module:
(components/qmldir in qt_quick_app_dev_intro_src.zip, see Downloads section)
Configure 1.0 Configure.qml
NightClock 1.0 NightClock.qml
WeatherModelItem 1.0 WeatherModelItem.qml
Weather 1.0 Weather.qml
Weather and WeatherModelItem include the code from the previous sections in a re-worked and extended form. We will take a closer look at the changes in the next section.
Moving code into a separate file is just the first step in making a component. You should decide and define how a new component should be used, i.e. which interfaces are provided to change its appearance and behavior. If you have used Qt with C++, you should keep in mind that using components in Qt Quick is different from using classes and libraries in C++. Qt Quick follows JavaScript in this as well. If you use an external component in your item, it is loaded almost as if it had been defined in-line taking over all its properties, handlers, signals, and so on. You can bind that existing property to another value, and use existing signals and handlers. You can also extend that component by declaring additional properties, new signals, handlers, and JavaScript functions. As all these steps are optional, a component has to have a default appearance and behavior if loaded as is. For example:
import QtQuick 1.1
import "components" 1.0
Item {
id: root
NightClock {
id: clock
}
}
This is the same as executing NightClock as a stand-alone Qt Quick application.
Let’s try this out and create a new application called clock-n-weather, which uses three components based on the code we developed earlier:
Most of the code for these components should not be new to you as we discussed them in the previous sections. NightClock remained unchanged. We are just binding a few of its properties (for example, showDate, showSeconds) to values from the root item and add new values to customize NightClock:
...
NightClock {
id: clock
height: 80
width: 160
showDate: root.showDate
showSeconds: root.showSeconds
textColor: Style.onlineClockTextColor
}
...
Properties showDate and showSeconds are configuration parameters, which we are used as property values of the root element. In a later section, we will add a Configure component to manage these as well as a few other values.
As mentioned above, the WeatherModelItem component uses the code from the application in the previous section, but works very differently. The rationale for making this change is to unite the forecast model and the current condition model in one component so that we can use them as a universal weather model:
(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()
}
}
}
In the code block above, you can see the two models being hosted under one item. We need to access them separately later when using them in views. If the imported WeatherModelItem has the weatherModelItem id, you might suggest to access them as weatherModelItem.forecast and weatherModelItem.current. Unfortunately, this will not work. The problem is that the child items of an imported component are by default not visible. A way to solve this problem is to use alias properties to export their id:
property alias forecastModel: forecast
property alias currentModel: current
Our models can now be accessed as weatherModelItem.forecastModel and weatherModelItem.forecastModel.
Scope and visibility of items, their properties and JavaScript objects are a very important aspect in Qt Quick. We strongly advise reading the QML Scope [http://qt-project.org/doc/qt-4.8/qdeclarativescope.html] article in Qt Documentation.
The article referenced above also explains how Qt Quick scope mechanism resolves name conflicts. It is important to keep those rules in mind. A good practice in a daily work is to always qualify the properties you bind to. This makes your application’s code easier for others to understand and avoid unexpected side effects. Doing this, you should write for example:
Item {
id: myItem
...
enable: otherItem.visible
}
instead of just:
Item {
id: myItem
...
enable: visible
}
There are other enhancements made to the code from the previous sections which are worth noting.
The most important one is that we’ve added a timer that triggers the reloading of both models:
Timer {
// note that this interval is not accurate to a second on a full minute
// as 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()
}
}
This timer is similar to the timer in the clock application and updates the weather data in the modes each root.interval seconds. root.interval is a configuration parameter defined as a property and bound to according value in the parent item.
We also have an updated delegate component for drawing weather conditions. The major change is to use local weather icons instead of loading them from the Internet. This has many advantages such as saving the bandwidth (if the application is running on a mobile device) or just a different look-n-feel which better meets our expectations and is less dependent on external content. We use a very nice set of weather icons from the KDE [http://www.kde.org] project. We rename them to match weather condition descriptions and add just a few statements in JavaScript to load them from the local file system:
Image {
id: icon
anchors.fill: parent
smooth: true
fillMode: Image.PreserveAspectCrop
source: "../content/resources/weather_icons/" +
conditionText.toLowerCase().split(' ').join('_') + ".png"
onStatusChanged: if (status == Image.Error) {
// we set the icon to an empty image
// if we failed to find one
source = ""
console.log("no icon found for the weather condition: \""
+ conditionText + "\"")
}
}
Notice that the Weather component can also be started as a stand alone Qt Quick application if needed. It uses the default property values in this case. This is useful for testing the component under various conditions. The weather component looks like this:
The main item of the complete application using components looks like this:
(clock-n-weather/ClockAndWeather.qml in qt_quick_app_dev_intro_src.zip, see Downloads section)
import QtQuick 1.1
import "../components" 1.0
import "../js/logic.js" as Logic
import "../js/style.js" as Style
Rectangle {
id: root
property string defaultLocation: "Munich"
property int defaultInterval: 60 // in seconds
property bool showSeconds: true
property bool showDate: true
width: 360
height: 640
Image {
id: background
source: "../content/resources/background.png"
fillMode: "Tile"
anchors.fill: parent
onStatusChanged: if (background.status == Image.Error)
console.log("Background image \"" +
source +
"\" cannot be loaded")
}
WeatherModelItem {
id: weatherModelItem
location: root.defaultLocation
interval: root.defaultInterval
}
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°"
}
}
Column {
id: clockAndWeatherScreen
anchors.centerIn: root
NightClock {
id: clock
height: 80
width: 160
showDate: root.showDate
showSeconds: root.showSeconds
textColor: Style.onlineClockTextColor
}
Repeater {
id: currentWeatherView
model: weatherModelItem.currentModel
delegate: weatherCurrentDelegate
}
GridView {
id: forecastWeatherView
width: 300
height: 300
cellWidth: 150; cellHeight: 150
model: weatherModelItem.forecastModel
delegate: weatherForecastDelegate
}
}
MouseArea {
anchors.fill: parent
onClicked: Qt.quit()
}
}
We load WeatherModelItem as weatherModelItem and define two delegates based on the Weather component. Then we have a Column [http://qt-project.org/doc/qt-4.8/qml-column.html] with our NightClock component, a Repeater [http://qt-project.org/doc/qt-4.8/qml-repeater.html] with the current weather condition data, and a GridView [http://qt-project.org/doc/qt-4.8/qml-gridview.html] with the forecast. That’s it! This is how it looks on the screen:
Using components is a powerful way to extend Qt Quick functionality. Qt Quick components are used to create a full set of UI elements on Symbian [http://doc.qt.digia.com/qtquick-components-symbian-1.1/index.html]. There is a dedicated page about this topic [http://qt-project.org/wiki/Qt_Quick_Components] on the Qt Project wiki. The Qt Components project [http://qt.gitorious.org/qt-components] on Gitorious hosts several other implementations, including Qt Components for desktop [http://qt.gitorious.org/qt-components/desktop].
What’s Next?
In the next chapter, we will focus on the interaction with the user. We will learn how Qt Quick supports this and how we can create simple UI components that suit our needs.