Qt Quick Carousel Tutorial

From Qt Wiki
Revision as of 08:55, 25 February 2015 by Maintenance script (talk | contribs)
Jump to navigation Jump to search


English Български

[toc align_right="yes" depth="3"]

The Qt Quick Carousel Tutorial

Introduction

This tutorial will show you how to create an animated menu with items rotating in a pseudo 3D space. You will also be shown how to break up a QML project into several modules as well as how to add support for different languages using Qt Linguist.

All you need is the qmlviewer tool in order to follow this tutorial. If you are running a recent version of Linux, simply install the qt4-qmlviewer and libqt4-dev packages. All files in this tutorial are available from "gitorious":https://www.gitorious.org/qt-training/qml-demos/trees/master/carousel .

The minimum required Qt Quick version is 1.0, which has been shipping with Qt since version 4.7.1.

Getting to know the PathView

The "PathView":http://developer.qt.nokia.com/doc/qt-4.7/qml-pathview.html element takes items out of a model and lays them out along a path. The path geometry is defined by a series of segments. There are three different types of segments: lines, quadratic and cubic "Bézier":http://en.wikipedia.org/wiki/Bezier_curve curves. To develop an understanding of how to construct a path, let us start with a single line segment:

// Carousel0.qml
import QtQuick 1.0

PathView {
 id: view
 width: 640
 height: 360
 model: 32
 delegate: Text { text: index }
 path: Path {
 startX: 0
 startY: 0
 PathLine { x: view.width; y: view.height }
 }
}

The PathView is our top-level element. Providing just an item count 32 as model automatically generates model indexes ranging from 0 to 31. For each item a delegate is instantiated to display that item. We start with simple text elements to display the index numbers. We will see later how to create more meaningful delegates — for now let us focus on how to create paths. The path specification requires at least a starting point and a single segment. In our case we start at (0, 0) and draw a single line segment to the bottom right. Loading this file with the qmlviewer gives you an output similar to: Carousel0.png

When you drag the items, the path view will kinematically scroll. Notice how it locks back into a fixed position. By default, the first item is centered at the starting point, while the rest of the items are distributed evenly along the path.

OK, let's add some complexity and draw a full rectangle. Replace the path specification from the previous example and add the following:

// Carousel1.qml

property int pathMargin: 50
path: Path {
 startX: pathMargin
 startY: pathMargin
 PathLine { x: view.width - pathMargin; y: pathMargin }
 PathLine { x: view.width - pathMargin; y: view.height - pathMargin }
 PathLine { x: pathMargin; y: view.height - pathMargin }
 PathLine { x: path.startX; y: path.startY }
}

You will see a rectangle. Each "PathLine":http://developer.qt.nokia.com/doc/qt-4.7/qml-pathline.html starts at the end point of the previous segment, so we only need to specify the end point for each line segment. For the last segment, we just reference the starting point to close the loop. We've introduced the custom property pathMargin to help define the path's geometry. Using custom properties is the easiest way to introduce helper variables in Qt Quick, but use this feature wisely because all properties in Qt Quick are public.

You might have wanted to write:


path: Path {
 property int pathMargin: 50
 startX: pathMargin

…but this is not possible as the Path component is already defined elsewhere and we are not allowed to add new properties on the fly.

The quadrature of the circle

Now that we've learned how to create a rectangle, let's try something a bit more difficult: a circle. The Path element itself does not support circular curves and therefore we have to approximate using one of the less favorable choices: lines, quadratic or cubic Bézier splines. As this is not a discussion about analytical geometry, let's jump to the best result in an instant: 4 cubic Bézier splines using the "magic number 0.551784":http://www.tinaja.com/glib/ellipse4.pdf. Take a look at the code:

// Carousel2.qml

property int pathMargin: 50
property real rx: ry // view.width / 2 - pathMargin
property real ry: view.height / 2 - pathMargin
property real magic: 0.551784
property real mx: rx * magic
property real my: ry * magic
property real cx: view.width / 2
property real cy: view.height / 2
path: Path {
 startX: view.cx + view.rx; startY: view.cy
 PathCubic { // first quadrant arc
 control1X: view.cx + view.rx; control1Y: view.cy + view.my
 control2X: view.cx + view.mx; control2Y: view.cy + view.ry
 x: view.cx; y: view.cy + view.ry
 }
 PathCubic { // second quadrant arc
 control1X: view.cx - view.mx; control1Y: view.cy + view.ry
 control2X: view.cx - view.rx; control2Y: view.cy + view.my
 x: view.cx - view.rx; y: view.cy
 }
 PathCubic { // third quadrant arc
 control1X: view.cx - view.rx; control1Y: view.cy - view.my
 control2X: view.cx - view.mx; control2Y: view.cy - view.ry
 x: view.cx; y: view.cy - view.ry
 }
 PathCubic { // forth quadrant arc
 control1X: view.cx + view.mx; control1Y: view.cy - view.ry
 control2X: view.cx + view.rx; control2Y: view.cy - view.my
 x: view.cx + view.rx; y: view.cy
 }
}

Which gives us a picture-perfect circle: Carousel2.png

As you can see, we need two control points for each cubic Bézier path segment. (All points are given in absolute coordinates.) If we had gone with quadratic Bézier curves, we only would have needed one single control point per segment. The control points lie outside the curvature, but influence its shape. You should be familiar with the concept from popular vector graphics tools.

Hiding complexity

A QML file "can be defined to be a re-usable component":http://doc.qt.nokia.com/4.7/qml-extending-types.html#defining-new-components. When you grow your code, you commonly want to split out generic elements into separate files to enable reuse and increase readability. In our example we would want to write:

// Carousel3.qml
import QtQuick 1.0

PathView {
 id: view
 width: 640
 height: 360
 model: 32
 delegate: Text { text: index }
 path: Ellipse {
 width: view.width
 height: view.height
 }
}

Our self-defined Ellipse component hides all the complexity of the circle approximation and scaling. When we define our own components derived from exiting ones, we can also introduce new properties. The Ellipse component contains the following code, which is a slightly generalized version of our earlier circle approximation:

// Ellipse.qml
import QtQuick 1.0

Path {
 id: p
 property real width: 200
 property real height: 200
 property real margin: 50
 property real cx: width / 2
 property real cy: height / 2
 property real rx: width / 2 - margin
 property real ry: height / 2 - margin
 property real mx: rx * magic
 property real my: ry * magic
 property real magic: 0.551784
 startX: p.cx; startY: p.cy + p.ry
 PathCubic { // second quadrant arc
 control1X: p.cx - p.mx; control1Y: p.cy + p.ry
 control2X: p.cx - p.rx; control2Y: p.cy + p.my
 x: p.cx - p.rx; y: p.cy
 }
 PathCubic { // third quadrant arc
 control1X: p.cx - p.rx; control1Y: p.cy - p.my
 control2X: p.cx - p.mx; control2Y: p.cy - p.ry
 x: p.cx; y: p.cy - p.ry
 }
 PathCubic { // forth quadrant arc
 control1X: p.cx + p.mx; control1Y: p.cy - p.ry
 control2X: p.cx + p.rx; control2Y: p.cy - p.my
 x: p.cx + p.rx; y: p.cy
 }
 PathCubic { // first quadrant arc
 control1X: p.cx + p.rx; control1Y: p.cy + p.my
 control2X: p.cx + p.mx; control2Y: p.cy + p.ry
 x: p.cx; y: p.cy + p.ry
 }
}

As you can see, we've decided to start in the second quadrant. The idea is to have the first item on the path shown in the bottom-most position, which is also the position of the current item.

Adding the artwork

At this point, you should have developed a basic understanding of how to manage paths. This leaves us with doing the real magic: bringing the artwork to life. We have chosen a small "tron-style":http://www.dirtydogicons.com/en/icons/7-tron-basic icon set and we will directly go ahead and make it appear in 3D. It is astonishingly simple in QML to do so.

First, let's create the necessary model. We've started with a simple dummy model and will now replace it with a real model. Here's the code:

// Model0.qml
import QtQuick 1.0

ListModel {
 ListElement { title: "Calendar"; iconSource: "icons/calendar.png" }
 ListElement { title: "Setup"; iconSource: "icons/develop.png" }
 ListElement { title: "Internet"; iconSource: "icons/globe.png" }
 ListElement { title: "Messages"; iconSource: "icons/mail.png" }
 ListElement { title: "Music"; iconSource: "icons/music.png" }
 ListElement { title: "Call"; iconSource: "icons/phone.png" }
}

We've chosen to use a "ListModel":http://developer.qt.nokia.com/doc/qt-4.7/qml-listmodel.html and we populate it with our custom list elements. We are free to define our own properties for the items. We'll use the iconSource to display the menu icon and the title to add a title display later-on. Our new delegate is based on the Image component and looks like the following:

// Carouse5.qml
import QtQuick 1.0

Rectangle {
 width: 640
 height: 360
 color: "black"
 PathView {
 id: view
 width: parent.width
 height: parent.height + y
 y: 33
 model: Menu0 {}
 delegate: Image {
 source: iconSource
 width: 64
 height: 64
 scale: 4. * y / parent.height
 z: y
 smooth: true
 opacity: scale / 2.
 }
 path: Ellipse {
 width: view.width
 height: view.height
 }
 }
}

Not much code, right? But it already has everything needed for a gorgeous 3D menu. We trick the eye into believing it is 3D by scaling the icons down towards the top. Reducing opacity in the distance also helps the depth perception. If you open Carousel4.qml in qmlviewer it gives you:

Carousel4.png

Adding keyboard interaction

Adding keyboard interaction is quite simple and straight forward. First we have to add the following three lines of code to process the keyboard events:

// Carousel5.qml

PathView {
 
 focus: true
 Keys.onLeftPressed: decrementCurrentIndex()
 Keys.onRightPressed: incrementCurrentIndex()
 
}

All the visual elements have a focus property. Only one element at a time should have it set to true and this element is the one that receives keyboard input events. We use the attached "Keys":http://developer.qt.nokia.com/doc/qt-4.7/qml-keys.html properties to implement the event handlers. We could also have used the more elaborate keyboard event handler:

Keys.onPressed: {
 if (event.key  Qt.Key_Left) decrementCurrentIndex()
    else if (event.key  Qt.Key_Right) incrementCurrentIndex()
 event.accepted = true
}

As you can see, we are counting the current index up or down when a key is pressed. But which one is actually the current item? As the path view can take up any abstract shape, how can it know which one should be the currently active item if any? The current item is also called the highlighted item and you have to add the following lines to tell the PathView which item to highlight:


PathView {
 
 preferredHighlightBegin: 0
 preferredHighlightEnd: 0
 highlightRangeMode: PathView.StrictlyEnforceRange
 
}

In our case, we decided to make the first item on the path the current item. Now, as we have defined the current item, we should be able to navigate the menu by either incrementing or decrementing the current index. Try it out by opening Carousel5.qml in the qmlviewer.

Fine tuning

Finally we'd like to display the title of the current item. The PathView has a highlightItem which we could use to create a highlight, but we will simply bind a "Text":http://developer.qt.nokia.com/doc/qt-4.7/qml-text.html item to the geometry of the root element to display the title. This also shows how you can link up components from different places.


PathView {
 id: view
 
}
Text {
 id: label
 text: view.model.get(view.currentIndex).title
 color: "paleturquoise"
 font.pixelSize: 16
 font.bold: true
 anchors.horizontalCenter: parent.horizontalCenter
 anchors.bottom: parent.bottom
}

Qt Quick provides a rich set of attributes for text styling. The color value paleturquoise is one of the "SVG color names":http://www.w3.org/TR/SVG/types.html#ColorKeywords. As you can see, we've bound the text directly to the title stored in our model. This type of binding is quick and easy. Whenever the currentIndex changes, QML will automatically reevaluate the text. But beware the risks here. Do not over-use direct bindings across different object hierarchies. For instance when we decide to rename the model attribute title to something else, we are breaking all property bindings which include the title and we won't get a warning until the component is actually instantiated.

Internationalization

There are many topics we could have talked about in this small tutorial, which was meant to be provide a quick tour through Qt Quick, but talking about things like C++ bindings, packaging or deployment would certainly have made us depart far from our initial goal of keeping things simple. There is, however, one important point we can still include here and that is internationalization. Usually a quite complex topic, Qt makes translating applications astonishingly simple.

Qt includes "Qt Linguist":http://developer.qt.nokia.com/doc/qt-4.7/linguist-manual.html, which is a powerful tool for translating applications. The same procedures used for C++ code also apply for QML with the exception of a "few differences":http://developer.qt.nokia.com/doc/qt-4.7/qdeclarative.html

Strings that should be translated into QML need to be wrapped with the qsTr() function. The lupdate tool is then able to extract those strings and generate XML translation files (.ts extension). The interpreter can then add translations using Qt Linguist and, once they have been added, the resulting .ts files are compiled into .qm files using lrelease. To test a translation simply pass the .qm file to the qmlviewer with the -translation command line option.

There are a few tricky parts along the road you have to know about, so let's walk through the process with our small carousel menu and translate the title texts shown below the current item. You might think you could just wrap the title text in the model like this:

ListModel {
 ListElement { title: qsTr("Calendar"); iconSource: "icons/calendar.png" }
 

But that's not going to work, because script expressions are not allowed for the property values of a list element. There are few obvious choices on how to work around that issue. As JavaScript functions are allowed practically everywhere we have chosen to rewrite the title property as a function:

// Menu1.qml
import QtQuick 1.0

ListModel {
 ListElement { iconSource: "icons/calendar.png" }
 ListElement { iconSource: "icons/develop.png" }
 ListElement { iconSource: "icons/globe.png" }
 ListElement { iconSource: "icons/mail.png" }
 ListElement { iconSource: "icons/music.png" }
 ListElement { iconSource: "icons/phone.png" }
 function title(index) {
 if (title["text"] === undefined) {
 title.text = [
 qsTr("Calendar"),
 qsTr("Setup"),
 qsTr("Internet"),
 qsTr("Messages"),
 qsTr("Music"),
 qsTr("Call")
 ]
 }
 return title.text[index]
 }
}

As you can see we call qsTr() for each menu item the first time the title() function is called. Of course you have to use such constructs only, if you are dealing with translating model values. All other strings can be wrapped directly with qsTr(). As we have changed the model definition, we also need to refactor the dependent code. In our case we are lucky and only need to replace one line:

text: view.model.title(view.currentIndex)

Now we call lupdate to generate the XML translation files:

lupdate . -ts Carousel_de.ts -codecfortr UTF-8

Then open this file with the linguist and enter your translations. Once you've finished, invoke lrelease:

lrelease Carousel_de.ts

The lrelease tool will generate a new Carousel_de.qml translation file. You can test the translation by invoking the qmlviewer from the command line like the following:

usr/bin/qmlviewer -translation Carousel_de.qm Carousel7.qml

Credits