Qt Quick Carousel Tutorial: Difference between revisions

From Qt Wiki
Jump to navigation Jump to search
No edit summary
(Changed only the name of "Model0.qml" to "Menu0.qml" as you are referencing it in "Carouse5.qml".)
 
(15 intermediate revisions by 4 users not shown)
Line 1: Line 1:
[[Category:Developing_with_Qt::Qt Quick]]
[[Category:Developing_with_Qt::Qt Quick]]
'''English''' [[:Qt_Quick_Carousel_Bulgarian|Български]]
[toc align_right="yes" depth="3"]
= The Qt Quick Carousel Tutorial =


== Introduction ==
== Introduction ==
Line 11: Line 6:
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.
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 .
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.


The minimum required Qt Quick version is 1.0, which has been shipping with Qt since version 4.7.1.
The minimum required Qt Quick version is 1.0, which has been shipping with Qt since version 4.7.1.
Line 17: Line 12:
== Getting to know the PathView ==
== 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:
The PathView 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 [http://en.wikipedia.org/wiki/Bezier_curve Bézier] curves. To develop an understanding of how to construct a path, let us start with a single line segment:


<code><br />// Carousel0.qml<br />import QtQuick 1.0
<code>
// Carousel0.qml
import QtQuick 2.1


PathView {<br /> id: view<br /> width: 640<br /> height: 360<br /> model: 32<br /> delegate: Text { text: index }<br /> path: Path {<br /> startX: 0<br /> startY: 0<br /> PathLine { x: view.width; y: view.height }<br /> }<br />}<br /></code>
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 }
}
}
</code>


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:<br />[[Image:http://www.gitorious.org/qt-training/qml-demos/blobs/raw/4ef3dccc2be296484845628880c5abcebbbceb1e/carousel/Carousel0.png|Carousel0.png]]
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.


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.
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:<br /><code><br />// Carousel1.qml<br /><br />property int pathMargin: 50<br />path: Path {<br /> startX: pathMargin<br /> startY: pathMargin<br /> PathLine { x: view.width - pathMargin; y: pathMargin }<br /> PathLine { x: view.width - pathMargin; y: view.height - pathMargin }<br /> PathLine { x: pathMargin; y: view.height - pathMargin }<br /> PathLine { x: path.startX; y: path.startY }<br />}<br /><br /></code>
OK, let's add some complexity and draw a full rectangle. Replace the path specification from the previous example and add the following:
<code>
// 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 }
}
</code>


You will see a rectangle. Each &quot;PathLine&amp;quot;: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 will see a rectangle. Each PathLine 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:<br /><code><br /><br />path: Path {<br /> property int pathMargin: 50<br /> startX: pathMargin<br /><br /></code>
You might have wanted to write:
<code>
path: Path {
property int pathMargin: 50
startX: pathMargin
</code>


…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.
…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.
Line 37: Line 67:
== The quadrature of the circle ==
== 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 &quot;magic number 0.551784&amp;quot;:http://www.tinaja.com/glib/ellipse4.pdf. Take a look at the code:<br /><code><br />// Carousel2.qml<br /><br />property int pathMargin: 50<br />property real rx: ry // view.width / 2 - pathMargin<br />property real ry: view.height / 2 - pathMargin<br />property real magic: 0.551784<br />property real mx: rx * magic<br />property real my: ry * magic<br />property real cx: view.width / 2<br />property real cy: view.height / 2<br />path: Path {<br /> startX: view.cx + view.rx; startY: view.cy<br /> PathCubic { // first quadrant arc<br /> control1X: view.cx + view.rx; control1Y: view.cy + view.my<br /> control2X: view.cx + view.mx; control2Y: view.cy + view.ry<br /> x: view.cx; y: view.cy + view.ry<br /> }<br /> PathCubic { // second quadrant arc<br /> control1X: view.cx - view.mx; control1Y: view.cy + view.ry<br /> control2X: view.cx - view.rx; control2Y: view.cy + view.my<br /> x: view.cx - view.rx; y: view.cy<br /> }<br /> PathCubic { // third quadrant arc<br /> control1X: view.cx - view.rx; control1Y: view.cy - view.my<br /> control2X: view.cx - view.mx; control2Y: view.cy - view.ry<br /> x: view.cx; y: view.cy - view.ry<br /> }<br /> PathCubic { // forth quadrant arc<br /> control1X: view.cx + view.mx; control1Y: view.cy - view.ry<br /> control2X: view.cx + view.rx; control2Y: view.cy - view.my<br /> x: view.cx + view.rx; y: view.cy<br /> }<br />}<br /><br /></code>
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 [http://www.tinaja.com/glib/ellipse4.pdf magic number 0.551784]. Take a look at the code:
 
<code>
Which gives us a picture-perfect circle:<br />[[Image:http://www.gitorious.org/qt-training/qml-demos/blobs/raw/4ef3dccc2be296484845628880c5abcebbbceb1e/carousel/Carousel2.png|Carousel2.png]]
// 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
}
}
</code>


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.
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.
Line 45: Line 109:
== Hiding complexity ==
== Hiding complexity ==


A QML file &quot;can be defined to be a re-usable component&amp;quot;: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:<br /><code><br />// Carousel3.qml<br />import QtQuick 1.0
A QML file can be defined to be a re-usable component. 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:
<code>
// Carousel3.qml
import QtQuick 2.1


PathView {<br /> id: view<br /> width: 640<br /> height: 360<br /> model: 32<br /> delegate: Text { text: index }<br /> path: Ellipse {<br /> width: view.width<br /> height: view.height<br /> }<br />}<br /></code>
PathView {
id: view
width: 640
height: 360
model: 32
delegate: Text { text: index }
path: Ellipse {
width: view.width
height: view.height
}
}
</code>


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:<br /><code><br />// Ellipse.qml<br />import QtQuick 1.0
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:
<code>
// Ellipse.qml
import QtQuick 2.1


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


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.
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.
Line 57: Line 172:
== Adding the artwork ==
== 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 &quot;tron-style&amp;quot;: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.
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 [http://www.dirtydogicons.com/en/icons/7-tron-basic tron-style] 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:<br /><code><br />// Model0.qml<br />import QtQuick 1.0
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:
<code>
// Menu0.qml
import QtQuick 2.1


ListModel {<br /> ListElement { title: &quot;Calendar&amp;quot;; iconSource: &quot;icons/calendar.png&amp;quot; }<br /> ListElement { title: &quot;Setup&amp;quot;; iconSource: &quot;icons/develop.png&amp;quot; }<br /> ListElement { title: &quot;Internet&amp;quot;; iconSource: &quot;icons/globe.png&amp;quot; }<br /> ListElement { title: &quot;Messages&amp;quot;; iconSource: &quot;icons/mail.png&amp;quot; }<br /> ListElement { title: &quot;Music&amp;quot;; iconSource: &quot;icons/music.png&amp;quot; }<br /> ListElement { title: &quot;Call&amp;quot;; iconSource: &quot;icons/phone.png&amp;quot; }<br />}<br /></code>
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" }
}
</code>


We've chosen to use a &quot;ListModel&amp;quot;: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:<br /><code><br />// Carouse5.qml<br />import QtQuick 1.0
We've chosen to use a ListModel 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:
<code>
// Carouse5.qml
import QtQuick 2.1


Rectangle {<br /> width: 640<br /> height: 360<br /> color: &quot;black&amp;quot;<br /> PathView {<br /> id: view<br /> width: parent.width<br /> height: parent.height + y<br /> y: –33<br /> model: Menu0 {}<br /> delegate: Image {<br /> source: iconSource<br /> width: 64<br /> height: 64<br /> scale: 4. * y / parent.height<br /> z: y<br /> smooth: true<br /> opacity: scale / 2.<br /> }<br /> path: Ellipse {<br /> width: view.width<br /> height: view.height<br /> }<br /> }<br />}<br /></code>
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
}
}
}
</code>


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:
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.
 
[[Image:https://www.gitorious.org/qt-training/qml-demos/blobs/raw/b0cdec015684c0d2a7042f11ff53eca683c3dc52/carousel/Carousel4.png|Carousel4.png]]


== Adding keyboard interaction ==
== 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:<br /><code><br />// Carousel5.qml<br /><br />PathView {<br /> <br /> focus: true<br /> Keys.onLeftPressed: decrementCurrentIndex()<br /> Keys.onRightPressed: incrementCurrentIndex()<br /> <br />}<br /><br /></code>
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:
<code>
// Carousel5.qml
PathView {
focus: true
Keys.onLeftPressed: decrementCurrentIndex()
Keys.onRightPressed: incrementCurrentIndex()
}
</code>


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 &quot;Keys&amp;quot;: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:<br /><code><br />Keys.onPressed: {<br /> if (event.key  Qt.Key_Left) decrementCurrentIndex()
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 [http://developer.qt.nokia.com/doc/qt-4.7/qml-keys.html Keys] properties to implement the event handlers. We could also have used the more elaborate keyboard event handler:
     else if (event.key  Qt.Key_Right) incrementCurrentIndex()<br /> event.accepted = true<br />}<br /></code>
<code>
Keys.onPressed: {
if (event.key  Qt.Key_Left) decrementCurrentIndex()
     else if (event.key  Qt.Key_Right) incrementCurrentIndex()
event.accepted = true
}
</code>


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:<br /><code><br /><br />PathView {<br /> <br /> preferredHighlightBegin: 0<br /> preferredHighlightEnd: 0<br /> highlightRangeMode: PathView.StrictlyEnforceRange<br /> <br />}<br /><br /></code>
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:
<code>
PathView {
preferredHighlightBegin: 0
preferredHighlightEnd: 0
highlightRangeMode: PathView.StrictlyEnforceRange
}
</code>


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'''.
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'''.
Line 84: Line 265:
== Fine tuning ==
== 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 &quot;Text&amp;quot;: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.<br /><code><br /><br />PathView {<br /> id: view<br /> <br />}<br />Text {<br /> id: label<br /> text: view.model.get(view.currentIndex).title<br /> color: &quot;paleturquoise&amp;quot;<br /> font.pixelSize: 16<br /> font.bold: true<br /> anchors.horizontalCenter: parent.horizontalCenter<br /> anchors.bottom: parent.bottom<br />}<br /><br /></code>
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 item to the geometry of the root element to display the title. This also shows how you can link up components from different places.
<code>
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
}
</code>


Qt Quick provides a rich set of attributes for text styling. The color value ''paleturquoise'' is one of the &quot;SVG color names&amp;quot;:http://www.w3.org/TR/SVG/types.html#ColorKeywords.<br />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.
Qt Quick provides a rich set of attributes for text styling. The color value ''paleturquoise'' is one of the [http://www.w3.org/TR/SVG/types.html#ColorKeywords SVG color names].
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 ==
== Internationalization ==
Line 92: Line 291:
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.
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 &quot;Qt Linguist&amp;quot;: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 &quot;few differences&amp;quot;:http://developer.qt.nokia.com/doc/qt-4.7/qdeclarative.html
Qt includes [http://doc.qt.io/qt-5/qtlinguist-index.html Qt Linguist], which is a powerful tool for translating applications. The same procedures used for C++ code also apply for QML with the exception of a [http://doc.qt.io/qt-4.8/qtdeclarative-module.html few differences]


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.
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:<br /><code><br />ListModel {<br /> ListElement { title: qsTr(&quot;Calendar&amp;quot;); iconSource: &quot;icons/calendar.png&amp;quot; }<br /> <br /></code>
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:
 
<code>
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:<br /><code><br />// Menu1.qml<br />import QtQuick 1.0
ListModel {
ListElement { title: qsTr("Calendar"); iconSource: "icons/calendar.png" }
</code>


ListModel {<br /> ListElement { iconSource: &quot;icons/calendar.png&amp;quot; }<br /> ListElement { iconSource: &quot;icons/develop.png&amp;quot; }<br /> ListElement { iconSource: &quot;icons/globe.png&amp;quot; }<br /> ListElement { iconSource: &quot;icons/mail.png&amp;quot; }<br /> ListElement { iconSource: &quot;icons/music.png&amp;quot; }<br /> ListElement { iconSource: &quot;icons/phone.png&amp;quot; }<br /> function title(index) {<br /> if (title[&quot;text&amp;quot;] === undefined) {<br /> title.text = [<br /> qsTr(&quot;Calendar&amp;quot;),<br /> qsTr(&quot;Setup&amp;quot;),<br /> qsTr(&quot;Internet&amp;quot;),<br /> qsTr(&quot;Messages&amp;quot;),<br /> qsTr(&quot;Music&amp;quot;),<br /> qsTr(&quot;Call&amp;quot;)<br /> ]<br /> }<br /> return title.text[index]<br /> }<br />}<br /></code>
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:
<code>
// Menu1.qml
import QtQuick 2.1


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()''.<br />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:<br /><code><br />text: view.model.title(view.currentIndex)<br /></code>
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]
}
}
</code>


Now we call '''lupdate''' to generate the XML translation files:<br /><code><br />lupdate . -ts Carousel_de.ts -codecfortr UTF-8<br /></code>
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:
<code>
text: view.model.title(view.currentIndex)
</code>


Then open this file with the '''linguist''' and enter your translations. Once you've finished, invoke '''lrelease''':<br /><code><br />lrelease Carousel_de.ts<br /></code>
Now we call '''lupdate''' to generate the XML translation files:
<code>
lupdate . -ts Carousel_de.ts -codecfortr UTF-8
</code>


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:<br /><code><br />usr/bin/qmlviewer -translation Carousel_de.qm Carousel7.qml<br /></code>
Then open this file with the '''linguist''' and enter your translations. Once you've finished, invoke '''lrelease''':
<code>
lrelease Carousel_de.ts
</code>


== Credits ==
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:
<code>
usr/bin/qmlviewer -translation Carousel_de.qm Carousel7.qml
</code>

Latest revision as of 02:36, 8 February 2018


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.

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 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 curves. To develop an understanding of how to construct a path, let us start with a single line segment:

// Carousel0.qml
import QtQuick 2.1

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.

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

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

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 2.1

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

// Menu0.qml
import QtQuick 2.1

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 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 2.1

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.

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

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 2.1

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