Model/view framework
Many applications need to display data to the user. Applications may even allow user to manipulate and create new data. Qt model/view framework makes it easy for the developer to create such applications. The model is separated from the view, allowing several views to share the same model or one view to change the model on the on-the-fly.
- Model is an adapter to the data and its structure. The actual data may be stored anywhere, for example in the database or some data center in the cloud. In trivial cases, the model itself can contain the data. QML contains several types for creating models, but for efficiency reasons a 'QAbstractItemModel' subclass is more frequently used.
- View displays the data in any kind of a visual structure, such as a list, table or path. There are views for stacking items StackView or scrolling large content ScrollView, but these views use internal models and provide a specialised API, so they are not covered here. Developers can create custom views as well to structure the UI in any way, they want.
- Delegate dictates how the data should appear in the view. The delegate takes each data item in the model and encapsulates it. The model data is accessible through the delegate.
Essential concepts
Dynamic views, like ListView, have a model and create and destroy delegate items dynamically to display data items. Any variant type can be assigned to the view's model
property. In the following example, an integer 5 is assigned to the model, resulting that the view will create five delegates. As the model may be any variant type, it is possible to assign a JavaScript, QList
, a QML model type or QAbstractItemModel
subclass, which will be covered in the next phase.
ListView {
anchors.fill: parent
model: 5
delegate: Component {
Text {
text: index
}
}
}
The delegate type is Component
. Components define re-usable, well-defined QML types similarly to QML files, but the type is defined as an inline file. The view manages the delegate item life time. Different views use different strategies to manage the life time and we will return to this in Chapter Views. Note that delegate
is a default property in ListView
and often omitted in the declaration.
An index
value is assigned to the text
property. Where did the index come from. The view exposes several properties to all delegate items. The ìndex
property is a context property, which means that each delegate item will have a different index value, even though the property name is the same. The first index is 0, the next one 1 and so on. This is a useful way to identify each delegate object.
Basic QML models
A simple and commonly used QML model type is ListModel, which is a container for ListElement definitions. In ListElement, the data is defined with named role definitions instead of properties, even though the syntax is the same. Note that the role names must be common with all other elements in a given model.
In the following example there are two roles, the name of a person and a color representing their team.
ListModel {
id: teamModel
ListElement { name: "Lars Knoll"; teamColor: "red" }
ListElement { name: "Alan Kay"; teamColor: "lightBlue" }
ListElement { name: "Trygve Reenskaug"; teamColor: "red" }
ListElement { name: "Adele Goldberg"; teamColor: "yellow" }
ListElement { name: "Ole-Johan Dahl"; teamColor: "red" }
ListElement { name: "Kristen Nygaard"; teamColor: "lightBlue" }
}
The type of a role is fixed the first time the role is used. All roles in the previous example are string types. Dynamic roles allow assigning different types to roles as well. To enable dynamic roles, the 'ListModel' property 'dynamicRoles' must be set to true. The data that is statically defined, like in the previous example, cannot have dynamic roles. The data items with dynamic roles must be added with JavaScript.
The view exposes roles to delegate items, which can refer to roles by just using the role names as in the following example. If Text
QML type contained a property called name
, model
qualifier must be used as a prefix to the role to avoid a name clash. An additional property modelData
is exposed to delegates, if the model is a list.
ListView {
anchors.fill: parent
model: teamModel
delegate: Text {
text: name
// text: modelData.name // in case there is a clash between roles and properties
}
}
Create an empty Qt Quick Controls application.
-
Add a
ListModel
and add at least oneListElement
. The list element should have a fileUrl role, which refers to image file URLs. -
Add a
ListView
filling the whole window area. -
Use a
BorderImage
delegate to show the images. All images can have a fixed size, e.g. 400 x 300 pixels.
Model manipulation
Creating and manipulating model data statically, as we did in earlier examples, is not scalable in real world applications. Data items can be manipulated in JavaScript, but more frequently C++ models are used as we will see in Phase 5. 'ListModel' has methods to append(), insert(), move(), and remove().
The following example shows, how to data to a ListModel
with append
:
ListModel {
id: teamModel
ListElement {
name: "Team C++"
}
}
MouseArea {
anchors.fill: parent
onClicked: {
teamModel.append({ name: "Team Quick" })
}
}
To create new items, you can use JSON { "teamName": "Team Qt", "teamColor": "green" }
or JavaScript notation { teamName: "Team Qt", teamColor: "green" }
. It should be noted that the first item inserted or declared to the model will determine the roles available to any views. In other words, if the model is empty and an item is added, only roles defined in that item are bound and used in subsequent added items. To reset the roles, the model needs to be emptied with the ListModel::clear() method.
This exercise is first of many during Part 4 where you add functionality to the PictureFrame application. It will not be tested in TMC, instead just start a fresh QML application and add things there every time a PictureFrame exercise comes up (this is first of 8, each part is worth 1 point, so total of 8 points are available). You can use the code from the last exercise as a starting point. The exercise will be returned as a GitHub repository to be peer-reviewed after the final part of it is done. You will then be graded based on which parts you did (i.e. if you get stuck you can skip one of the parts and continue).
Instructions: Add an action to menu to add and remove images from the model.
- The add image action should open a file dialog, where the user can choose multiple image files to be added to the model.
- The remove action should remove the last image from the model. Do not try to remove any image from an empty model.
-
Hint: Adding actions and a menu bar is easy, if you change
Window
toApplicationWindow
.
Threaded model manipulation
Modifying the model can be computationally expensive in some cases, and because we are doing it inside the UI thread, it can end up being blocked for quite a while. Consider a trivial case of creation 1,000,000 items into the model. Computationally heavy updates should preferably be done inside a background thread. For example, if we were to implement a Twitch chat client, we would run into performance problems very fast if we were to use the main thread for updating the message model:
TwitchChat {
// signal messageReceived(string user, string message)
onMessageReceived: {
// Massage the received message data inside the signal handler ...
// Remove old messages from backlog ...
if (messageModel.count > 2147483647)
messageModel.remove(0, 1337)
// Append the message data to the model
messageModel.append({"user": user, "message": message})
}
}
Qt provides a QML type WorkerScript to offload any heavy data and model manipulation out of the main thread. Messages are passed between the parent thread and new child thread with the sendMessage()
method and onMessage()
handler:
TwitchChat {
// signal messageReceived(string user, string message)
onMessageReceived: {
worker.sendMessage({"user": user, "message": message, "model": messageModel)
}
}
WorkerScript {
id: worker
source: 'script.js'
}
The onMessage
handler is implemented in the script.js
file which is invoked when sendMessage()
is called:
// script.js
WorkerScript.onMessage = function(msg) {
// Massage the received message data ...
// Remove old messages from backlog ...
if (msg.model.count > 2147483647)
msg.model.remove(0, 1337)
// Append the message data to the model
msg.model.append({"user": user, "message": message});
msg.model.sync(); // Needs to be called!
}
Notice that the script.js
file is not imported anywhere! This brings in the restrictions of this threading model: the source
property is evaluated on its own outside of QML. This means any properties or methods, and all data needs to be passed to sendMessage()
. The sendMessage()
supports all JavaScript types and objects. Arbitrary QObject
types are not supported, only the ListModel
type. Also, if you want your modifications to take effect in the ListModel
, you must call sync()
on it on the worker thread.
Add yet another action to the menu. When triggered, the action should add 10,000 copies of one of the model fileUrls to the model.
- Obviously, the UI must be kept responsive all the time.
Exhaustive reference material mentioned in this topic
https://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.html
https://qmlbook.github.io/en/ch06/index.html
http://doc.qt.io/qt-5/qml-qtqml-models-listmodel.html
QML Models
There are several other QML model types inn addition to ListModel
. XmlistModel
allows construction of a model from an XML data source. It provides a very convenient way to parse XML data. ÒbjectModel contains the visual items to be used in a view. No delegate is required as the model already contains visual items.
DelegateModel is useful in cases, where ´QAbstractItemModel
index is used to access data items. The typical use case are hierarchical tree models, where the user needs to navigate up and down the subtree.
It is possible to use an object instance as a model as well. In this case, object properties are provided as roles as in the following example.
Text {
id: myText
text: "Hello"
visible: false // we want to show the text in the view
}
Component {
id: myDelegate
Text { text: model.text } // the qualifier needed
}
ListView {
anchors.fill: parent
model: myText
delegate: myDelegate
}
XmlListModel
XmlListModel is used to create a read-only model from XML data. It's a very convenient way to read data from RSS feeds and create a data model from relevant XML elements.
A simple XML structure could be the following:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<item>
<title>A blog post</title>
<pubDate>Sat, 07 Sep 2010 10:00:01 GMT</pubDate>
</item>
<item>
<title>Another blog post</title>
<pubDate>Sat, 07 Sep 2010 15:35:01 GMT</pubDate>
</item>
</channel>
</rss>
The XML document contains tags (<rss>
), which can have attributes (version="2.0"
). Start tag <item>
and end tag </item>
form an element, and an element can have child elements. This basically means we have a tree of nodes, and we need to traverse the tree to extract data.
The following example shows, how to create a model from XML data.
import QtQuick 2.0
import QtQuick.XmlListModel 2.0
XmlListModel {
id: xmlModel
source: "http://www.mysite.com/feed.xml"
query: "/rss/channel/item"
XmlRole { name: "title"; query: "title/string()" }
XmlRole { name: "pubDate"; query: "pubDate/string()" }
}
The source
property defines the location for the XML document, which can be either local or remote resource.
The query
property value should contain a valid XPath (XML Path Language) selector. Among other things, XPath is used to select nodes from the XML document tree that match a given path expression.
With the query of /rss/channel/item
we select any <item>
that are children of <channel>
that are children of the document root element <rss>
to be the items for the model. See XPath usage examples for more detailed usage.
The roles are defined with XmlRoles. Notice that the <item>
elements contain other arbitrary elements, so we need a query to bind specific data. In the example, we query the <title>
and <pubDate>
elements inside the <item>
, get their values with the string()
function and bind the value to the aptly named roles title
and pubDate
. See XmlRole::query for more examples.
Using the generated models in ListView
works as usual. The delegate can bind to model roles.
ListView {
width: 180; height: 300
model: xmlModel
delegate: Text { text: title + ": " + pubDate }
}
Implement a news reader app. The news are available in http://feeds.bbci.co.uk/news/rss.xml. Any other feed may be used, but the feed must provide at least some text data + URLs to get further information.
-
Read all
items
tags from the feed. -
Get the news
title
andlink
from the news items. - The delegate should show the title and link in a column.
- When the user clicks on the delegate item, the link is opened in the Qt web engine.
-
Split the screen vertically between the view and web engine. Note that to be able to use the web engine in QML, you must call
QtWebEngine::initialize()
in C++.
The tests for this exercise don't actually test much, submit it when you've accomplished the task. We'll do spot-checks to make sure you don't just return an empty program, so, remember to actually try :)
Object Model
ObjectModel defines a set of items rather than non-visual list elements. This makes it unnecessary to use delegates.
ObjectModel {
id: itemModel
Text { color: "red"; text: "Hello " }
Text { color: "green"; text: "World " }
Text { color: "blue"; text: "again" }
}
ListView {
anchors.fill: parent
model: itemModel
orientation: Qt.Horizontal
}
Delegate Model
DelegateModel is similar to ObjectModel
in that sense that it also encapsulates the model and delegate together. In addition, it is used in hierarchical models. Note that all QML model types are actually lists. Table, tree, and hierarchical models can be created by subclassing QAbstractItemModel
. DelegateModel
has a rootIndex
property, which is a QModelIndex
type. The model index allows navigating between child and parent items in the tree structure. This is covered in more detail in Phase 5.
DelegateModel
is also useful for sharing data items to delegates in multiple views or for sorting and filtering items.
Sharing data items
DelegateModel
uses a Package
QML type to enable delegates with a shared context to be provided to multiple views. Any item within a Package
may be assigned a name via the Package.name
attached property, similarly to the following example. There are two items with different package names. The actual content Text
is parented based on the delegate index to either a package, containing odd or to a package, containing even indices. The model myModel
contains just list elements with a display
role.
DelegateModel {
id: visualModel
delegate: Package {
Item { id: odd; height: childrenRect.height; Package.name: "oddIndex" }
Item { id: even; height: childrenRect.height; Package.name: "evenIndex" }
Text {
parent: (index % 2) ? even : odd
text: display
}
}
model: myModel
}
Now it is possible to use package names in different views. The DelegateModel
property parts
selects a DelegateModel
which creates delegates from the part named.
ListView {
height: parent.height/2
width: parent.width
model: visualModel.parts.oddIndex
}
ListView {
y: parent.height/2
height: parent.height/2
width: parent.width
model: visualModel.parts.evenIndex
}
Sorting and filleting data items
Delegates may be sorted and filtered using groups in DelegateView
. Each delegate belongs to an items
group by default. Additional groups can be defined using DelegateModelGroup
QML type. It provides methods to create and remove items in the group or move items to a different index position inside the group.
The first step to use groups is to add one or more groups to DelegateModel
. In the following example, there are two groups and by default all the delegate items are added to the group group1
. Note that items are also added to the default items
group, so any delegate may belong to several groups. The DelegateModel
property filterOnGroup
determines, we are interested in delegates in group1
only.
DelegateModel {
id: visualModel
model: myModel // contains ListElements with the display role
filterOnGroup: "group1"
groups: [
DelegateModelGroup { name: "group1"; includeByDefault: true },
DelegateModelGroup { name: "group2" }
]
For every group defined in DelegateModel
two attached properties are added to each delegate item. The first of the form DelegateModel.inGroupName
holds whether the item belongs to the group and the second DelegateModel.groupNameIndex
holds the index of the item in that group.
The attached properties are useful in checking and changing the group, to which the delegate belongs to or to change the position of an item in that group. The following example shows, how the group can be changed by a mouse press.
delegate: Text {
id: item
text: display
MouseArea {
anchors.fill: parent
onClicked: {
if (item.DelegateModel.inGroup1) {
item.DelegateModel.inGroup1 = false;
item.DelegateModel.inGroup2 = true;
}
else {
item.DelegateModel.inGroup1 = true;
item.DelegateModel.inGroup2 = false;
}
}
}
}
The group filter can be changed to display delegates in any of the groups. Only one group can be displayed at the time time.
ListView {
anchors.fill: parent
model: visualModel
focus: true
Keys.onReturnPressed: {
visualModel.filterOnGroup = (visualModel.filterOnGroup === "group1") ? "group2" : "group1";
}
}
You have tons of pictures and your picture frame does not provide any support to organise images. Let's add support for at least viewing different image types in different views.
- Split the list view in PictureFrame to two list views horizontally.
-
Remove the delegate and create a new
DelegateModel
. - Split images into two categories, e.g. .jpg and other images.
- One view should show .jpg files or whatever format you have chosen. The other view shows other images.
- Identify the image type from the file name extension.
Exhaustive reference material mentioned in this topic
https://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.html#xml-model
http://doc.qt.io/qt-5/qml-qtquick-xmllistmodel-xmllistmodel.html
https://qmlbook.github.io/en/ch06/index.html#a-model-from-xml
http://doc.qt.io/qt-5/xquery-introduction.html
http://doc.qt.io/qt-5/qml-qtqml-models-objectmodel.html
http://doc.qt.io/qt-5/qml-qtqml-models-delegatemodel.html
Views
For dynamic views QML provides four commonly used types, ListView
, GridView
, TableView
, and PathView. First three inherit
Flickable type, which enables users to flick content either horizontally or vertically. Note that there is a
TableView type in Qt Quick Controls, but those types are deprecated and not covered here.
PathView` allows organising data items in any path shape.
Each view may take any of the previously described QML model type and organise and decorate delegate items in the UI. All QML models are lists. TableView
is the only type, which can directly show several columns from a C++ table model, i.e. QAbstractItemModel
subclass. This is not any limitation though, as other views may use delegates, which display model columns, mapped to different roles.
In this chapter, we concentrate on use cases, where the view uses a QML model. C++ models are covered in Phase 5.
ListView
ListView organises delegates in a horizontal or vertical list. ListView
provides a variety of ways to decorate lists. Data items may be grouped into sections for better visualisation. The list may have a special header and footer item, and a highlight, indicating the current item.
In the following example, the clip
property is set to true
. In QML, parent items do not clip their children, so children may paint outside the bounding box of a parent. In most cases, this is ok. However, if there is, e.g. a tool bar and list view in the same UI window, quite likely we do not want the list view items to be painted on the top of the tool bar. Settings clip
to true
clips children, which paint outside the list view area.
ListView {
anchors.fill: parent
clip: true
model: 50
delegate: someNiceDelegate
}
Attached properties
Each delegate gets an access to a number of attached properties, provided by the view. For example, ListView
to which the delegate is bound is accessible from the delegate through the ListView.view
property. Or a delegate can check, whether it is the current item by using ListView.isCurrentItem
.
Attach properties allow implementing clean, re-usable delegates, which do not have direct bindings to the view via an id
, for example.
The attached properties are available to the root delegate item. Child objects must refer to attached properties using the root delegate identifier as in the following example. Sometimes a new custom var property, bound to an attached property, id declared. Remember however that each additional property increases the memory consumption.
ListView {
width: 300; height: 200
model: contactsModel
delegate: contactsDelegate
Component {
id: contactsDelegate
Rectangle {
id: rootWrapper
width: 80
height: 80
// Rectangle is the root item, we can refer to the attached property directly
color: ListView.isCurrentItem ? "black" : "red"
Text {
id: contactInfo
text: name + ": " + number
// Attached property is not accessible in the child
color: rootWrapper.ListView.isCurrentItem ? "red" : "black"
}
}
}
}
Sections
Grouping data items into sections improve readability in a list view. For example, albums grouped by artist names, or employees grouped by the department would easily show, which items are related.
When using sections, two properties need to be considered. In ListView
the property section.property
defines which property is used to divide the data into sections. It's important to note that the model needs to be sorted so that each property forming a section is continuous. If a property forming the section appears in multiple non-continuos places in the model, the same section might appear multiple times in the view. The second property to be considered is section.criteria
. It's set to ViewSection.FullString
by default, which means that the whole property is used as the section. If set to ViewSection.FirstCharacter
only the first character in the property is used for the section, for example when dividing up a list of names to sections based on the first letter of the last name.
After the sections have been defined, they can be accessed from the items using the attached properties ListView.section
, ListView.previousSection
, and ListView.nextSection
.
We can also assign a special section delegate to create a header before each section, by assigning it to the property section.delegate
in the ListView
.
In the following example, the data model has a list of artist and their albums, which are divided into sections by artist.
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: albumsAndArtists
delegate: albumDelegate
section.property: "artist"
section.delegate: sectionDelegate
}
Component {
id: albumDelegate
Item {
width: ListView.view.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
font.pixelSize: 12
text: album
}
}
}
Component {
id: sectionDelegate
Rectangle {
width: ListView.view.width
height: 20
color: "lightblue"
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 6
font.pixelSize: 14
text: section
}
}
}
ListModel {
id: albumsAndArtists
ListElement { album: "Crazy World"; artist: "Scorpions"; }
ListElement { album: "Love at First Sting"; artist: "Scorpions"; }
ListElement { album: "Agents of Fortune"; artist: "Blue Öyster Cult"; }
ListElement { album: "Spectres"; artist: "Blue Öyster Cult"; }
ListElement { album: "The Vale of Shadows"; artist: "Red Raven Down"; }
ListElement { album: "Definitely Maybe"; artist: "Oasis"; }
}
This time continue the PictureFrame application by adding sections to it.
-
Organise images into groups using
ListView
sections. - There is no need to group images in the model, so there may be any number of .tiff sections in the list view.
- Change the worker script as well to support sections.
Headers and Footers
Views allow visual customisation through decoration properties such as the header
and footer
. By binding an object, usually another visual object, to these properties, the views are decoraable. As an example, a footer may include a Rectangle
type showcasing borders, or a header that displays a logo on top of the list. It should be noted that headers and footers don't respect the spacing
property in ListView
, and thus any spacing needs to be a part of the header/footer itself.
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 4
delegate: numberDelegate
spacing: 2
header: headerComponent
footer: footerComponent
}
Component {
id: headerComponent
Rectangle {
width: ListView.view.width
height: 20
color: "lightBlue"
Text { text: 'Header'; anchors.centerIn: parent; }
}
}
Component {
id: footerComponent
Rectangle {
width: ListView.view.width
height: 20
color: "lightGreen"
Text { text: 'Footer'; anchors.centerIn: parent; }
}
}
Component {
id: numberDelegate
Rectangle {
width: ListView.view.width
height: 40
border.color: "black"
Text { text: 'Item ' + index; anchors.centerIn: parent; }
}
}
Add a footer to the PictureFrame application.
- The footer should be always visible, if there are any items in the model. It should not go out of the view, when the list is scrolled.
-
The footer must not be overpainted by the view and it must show the url of the current image in a
Label
Qt Quick Control.
Keyboard navigation and highlighting
When using a keyboard to navigate in the ListView, some form of highlighting is necessary to tell which item is currently selected. Two things are necessary to allow keyboard navigation in the ListView. First, the view needs to be given keyboard focus with the property focus: true
, and second a special highlight delegate needs to be defined. This is demonstrated in the following example:
ListView {
id: view
anchors.fill: parent
anchors.margins: 20
clip: true
model: 20
delegate: numberDelegate
spacing: 5
highlight: highlightComponent
focus: true
}
Component {
id: highlightComponent
Rectangle {
color: "lightblue"
radius: 10
}
}
Component {
id: numberDelegate
Item {
width: ListView.view.width
height: 40
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
The highlight delegate is given the x
, y
and height
of the current item. If the width
is not specified, the width of the current item is used.
Add a highlight into the PictureFrame application.
- The highlight should be a red ellipse, shown in the middle, right border of the image. You may add small margin that the ellipse does not overlap the border.
- The highlight size may be hard-coded, e.g. to 10 x 10 pixels.
- Animate the highlight position change using an out bounce easing curve.
GridView and TableView
GridView is largely similar to ListView
, and it's used in almost the same way. The main difference is, that it doesn't rely on spacing and size of delegates, and instead cellWidth
and cellHeight
are defined in the view. Header, footers, keyboard navigation with highlighting are all available in GridView
. Orientation is set in the flow
property, options being GridView.LeftToRight
(default) and GridView.TopToBottom
. The following example showcases the usage of GridView
.
GridView {
id: grid
anchors.fill: parent
cellWidth: 80;
cellHeight: 80
model: 100
delegate: numberDelegate
highlight: Rectangle { color: "lightsteelblue"; radius: 5 }
focus: true
}
Component {
id: numberDelegate
Item {
width: grid.cellWidth
height: grid.cellHeight
Text {
text: index
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
}
}
}
TableView can display multiple columns, provided that the underlying model has multiple columns. There is no QML model type with multiple columns, so the model must inherit QAbstractItemModel
.
The GridView
and TableView
exercise is postponed to Phase 4, where we compare how the views manage the lifetime of delegates.
PathView
PathView is the most powerful and customisable of the dynamic views provided by QML. PathView
will display the model data on a Path that can be arbitrarily defined with the Path
QML type. PathView
can be customised through a wide variety of properties, such as pathItemCount
, which controls the numbers of visible items at once, and preferredHighlightBegin
and preferredHighlightEnd
, which are used to control where along the path the current item is shown. The properties expect real values between 0 and 1. Setting both of them to, for instance, 0.5 would display the current item at the location 50% along the path.
A Path
is the path that the delegates follow when the view is scrolled. It defines the path using startX
and startY
properties, alongside path segments that are defined types such as PathQuad or PathLine. All the different path segments can be found in the Qt documentation. Qt Quick Designer commercial version even has an editor to create and remove path segments and define their shapes.
The following example shows items on a straight path.
PathView {
id: view
model: 20
anchors.fill: parent
path: Path {
startX: 0
startY: height
PathCurve {
x: view.width
y: 0
}
}
delegate: Text {
text: "Index " + index
}
}
When the path has been defined, we can further tune it using PathPercent
and PathAttribute
types. These objects can be placed in between the path segments to provide a more fine grained control over the path and the delegates. The PathPercent
allows you to manipulate the spacing between items on a PathView's path. You can use it to bunch together items on part of the path, and spread them out on other parts of the path. The PathAttribute
object allows attributes consisting of a name and a value to be specified for various points along the path. The attributes are exposed to the delegate as Attached Properties, and can be used to control any property. The value of an attribute at any particular point along the path is interpolated from the PathAttributes
bounding that point.
Next we have a larger example of PathView
, where the path is defined with PathQuad
and the size and opacity of the items is changed with PathAttribute
. We've also enabled keyboard navigation, which is not available by default. It can be done by giving the keyboard focus by setting the focus
property to true, and defining what the keys should do (in this case Keys.onLeftPressed: decrementCurrentIndex()
and Keys.onRightPressed: incrementCurrentIndex()
to allow moving back and forth).
// ContactModel.qml
ListModel {
ListElement { name: "Linus Torvalds"; icon: "pics/qtlogo.png"; }
ListElement { name: "Alan Turing"; icon: "pics/qtlogo.png"; }
ListElement { name: "Margaret Hamilton"; icon: "pics/qtlogo.png"; }
ListElement { name: "Ada Lovelace"; icon: "pics/qtlogo.png"; }
ListElement { name: "Tim Berners-Lee"; icon: "pics/qtlogo.png"; }
ListElement { name: "Grace Hopper"; icon: "pics/qtlogo.png"; }
}
Rectangle {
width: 250; height: 200
Component {
id: delegate
Item {
width: 80; height: 80
scale: PathView.iconScale
opacity: PathView.iconOpacity
Column {
Image { anchors.horizontalCenter: nameText.horizontalCenter; width: 64; height: 64; source: icon }
Text { id: nameText; text: name; font.pointSize: 12 }
}
}
}
PathView {
anchors.fill: parent
model: ContactModel {}
delegate: delegate
focus: true
Keys.onLeftPressed: decrementCurrentIndex()
Keys.onRightPressed: incrementCurrentIndex()
path: Path {
startX: 120
startY: 100
PathAttribute { name: "iconScale"; value: 1.0 }
PathAttribute { name: "iconOpacity"; value: 1.0 }
PathQuad { x: 120; y: 25; controlX: 260; controlY: 75 }
PathAttribute { name: "iconScale"; value: 0.3 }
PathAttribute { name: "iconOpacity"; value: 0.5 }
PathQuad { x: 120; y: 100; controlX: -20; controlY: 0 }
}
}
}
When using Image
QML type in the PathView
delegates, it is useful to bind the smooth
property to PathView.view.moving
. Less processing power is spent on smooth transformations, when the view is in motion, while images are smoothly transformed when stationary.
Let's improve the photo browsing in the PictureFrame application. As images are hard-coded to be quite large, it's difficult to quickly find the right picture. Let's add a PathView
to improve the browsing functionality.
- Split the window horizontally into to equal size areas. Keep the list view on the left and add path view to the right.
- Implement the path view into its own file.
- Implement a U-shape path using three path lines. The first line goes diagonally down from the top left corner. The second path line goes horizontally to the right and the third line goes diagonally to the top right corner.
- Change the delegate opacity and size to 50 per cent in diagonal lines.
Exhaustive reference material mentioned in this topic
https://qmlbook.github.io/en/ch06/index.html
https://doc.qt.io/qt-5/qml-qtquick-listview.html
https://doc.qt.io/qt-5/qml-qtquick-gridview.html
https://doc.qt.io/qt-5/qml-qtquick-pathview.html
https://doc.qt.io/qt-5/qml-qtquick-tableview.html
Delegates
Delegates act as templates for instantiating the visual items inside the view. The data roles provided by the model are bound to visual item properties, such as the Text.text
or Image.source
property. Delegates can also be used to modify and update the data bound to the roles by simply assigning new values to roles. The model will notify all views about the changed data values.
In the following example, we have a TeamDelegate
that updates the bound data when clicked:
ListModel {
id: teamModel
ListElement {
teamName: "Team C++"
teamColor: "blue"
}
}
ListView {
anchors.fill: parent
model: teamModel
delegate: TeamDelegate {}
}
// TeamDelegate.qml
Component {
Text {
text: teamName
color: teamColor
MouseArea {
anchors.fill: parent
onClicked: {
model.teamName = "Team Quick"
model.teamColor = "red"
}
}
}
}
}
Within the delegate, it is possible to access the data roles as internal properties. Remember also the model
qualifier, if the delegate item property names and model roles have a name clash.
Delegate size
In most examples so far, we have used Text
or Image
QML types for delegates. These types have implicit size, so there has not been any need to define the size explicitly using the Item::width
and Item::height
properties. In fact, using the implicit size and setting the explicit size in Text
incurs a performance penalty as the text must be laid out twice.
In real application, delegates seldom contain just a text or an image. More often delegates are composed from a few other QML types. As delegates are components, they must contain exactly one root item. The Item
QML type is a good candidate for the root item, as it's suitable for composing delegates from other items. However, Item
has no explicit size, so it has to be explicitly declared.
One approach is to bind the root item size to the view size. In the following example, we bind the delegate height to the height available for one item, assuming the model has n
items. To keep the text readable, we have used FontMetrics
to calculate the minimum height for the delegate.
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 50
delegate: numberDelegate
}
FontMetrics {
id: fontMetrics
font { family: "Arial"; pixelSize: 14 }
}
Component {
id: numberDelegate
Item {
width: ListView.view.width
height: Math.max(ListView.view.height / ListView.view.count, fontMetrics.height)
Rectangle {
anchors.fill: parent
border.color: "black"
Text {
font { family: "Arial"; pixelSize: 14}
text: 'Item ' + index;
anchors.centerIn: parent;
}
}
}
}
Another approach is to let the delegate children determine its size. This is useful, if children have implicit size like the Text
item in the following example.
Component {
id: numberDelegate
Item {
id: rootItem
width: ListView.view.width
height: childrenRect.height
Rectangle {
width: rootItem.width
height: childrenRect.height + 2
border.color: "black"
Text {
font { family: "Arial"; pixelSize: 14}
text: 'Item ' + index;
}
}
}
}
Keep your delegates as simple as possible. This applies to size calculation as well. Avoid complex size calculations and dependencies between delegate objects.
Let's change the PictureFrame exercise from the previous chapter to have little bit more complicated delegate. This will not be the best way to implement the delegate, but it's used for learning purposes. The goal is the understand, how delegate size should be determined.
-
Let's modify the path view delegate. The delegate root should be an
Item
QML type. -
The item should contain one child, which is
Column
, which is shown immediately below the border image. - The column contains the border image and a rectangle, which has a label as a child. The rectangle should use the image width and label height. The label is entered in the rectangle.
- The only hard-coded size can be the border image. It's totally acceptable to make the border image scalable as well, but it's not required in the exercise.
-
All other delegate objects must have either implicit size or the
width
andheight
properties bound to other properties. No hard-coded magic numbers are accepted.
This is the final part of Picture Frame exercise series. Now it's time to submit it to be peer-reviewed. This is done in the same manner as with the Directory Browser exerice at the end of Part 2. Create a GitHub repository for your project, and submit a link to it below.
Clip
The view clip
property will ensure that any view items outside of the view will not be visible. If set to false
, items will 'flow over' the view. Avoid using clip
in delegates. If clip
is enabled inside a delegate, each delegate will be batched separately. The QML renderer creates a scene graph of all the visible items. It tries to minimise the number of OpenGL state changes, by batching paint operations. One batch contains only operations, which do not require an OpenGL state changes. Less batches result to better rendering performance.
Clipping results that a scene graph node and its complete sub-tree will be put into one batch. If clip
is used in the view, but not in the delegates, the whole tree of the view and delegates can be put into one batch. However, as soon as a delegate uses clip
, a new scene graph sub-tree is created for each delegate. this will increase the number of batches and have negative impact on the performance.
Scenegraph renderer describes batching in more detail.
This is a voluntary exercise that will not be graded. We still recommend you do it though, the goal is to get familiar with the utilities to find and optimise batches in QML application!
-
Define an environment variable
QSG_RENDERER_DEBUG=render
and run your PictureFrame application in Qt Creator in Debug Mode. You will see the number of batches sent to OpenGL in Debug Console. -
Use the
QSG_VISUALIZE
environment variable to visualise the batches. The variable may have one of the four values:batches
,clip
,overdraw
orchanges
. Set theclip
property totrue
in your view. Check, if this affects the number of batches in any way.
Memory management
Dynamic views create and destroy delegates dynamically. The only exception is TableView
, which is able to re-use existing delegates from the pool as well. TableView
has the reuseItems
property to control this.
ListView
, GridView
, and PathView
will create as many delegate items, as the view is able to show in its area. This allows Qt developers to use huge item models, consisting of millions of items, as only a small portion of items is visible and created at one time. When a user flicks the view, visible items go beyond the view are and they are destroyed. New items will be created and they become visible in the view.
Dynamic creation of items implies you must never store state information into a delegate. Always store the state information into the model, before the item is destroyed.
Component.onDestruction: {
someBooleanRole = (state === "false") ? false : true;
}
Views provide caches to improve the performance, when the user is flicking the view. ListView
and GridView
use cacheBuffer
, while PathView
uses cacheItemCount
. PathView::pathItemCount
determines how many visible items are created and PathView::cacheItemCount
determines how many additional items are created in the cache. Caching delegate items improves the performance, but increases the memory consumption.
The property cacheBuffer
is an integer, determining how many delegate items are cached. If the value is 100 in a list view and the delegate height is 20, 5 items will be cached before and 5 items will be cached after the currently visible items.
In GridView
, the caching principle is similar. If in a vertical view the delegate is 20 pixels high, there are 3 columns and cacheBuffer
is set to 40, then up to 6 delegate items above and 6 delegate items below the visible area may be created or retained.
TableView
can also reuse delegate items. When an item is flicked out, it moves to the reuse pool, which is an internal cache of unused items. When this happens, the TableView::pooled
signal is emitted to inform the item about it. Likewise, when the item is moved back from the pool, the TableView::reused
signal is emitted.
Any item properties that come from the model are updated when the item is reused. This includes index, row, and column, but also any model roles.
Avoid storing any state inside a delegate. If you do, reset it manually on receiving the TableView::reused
signal.
If an item has timers or animations, consider pausing them on receiving the TableView::pooled
signal. That way you avoid using the CPU resources for items that are not visible. Likewise, if an item has resources that cannot be reused, they could be freed up.
The following example shows a delegate that animates a spinning rectangle. When it is pooled, the animation is temporarily paused.
Component {
id: tableViewDelegate
Rectangle {
implicitWidth: 100
implicitHeight: 50
TableView.onPooled: rotationAnimation.pause()
TableView.onReused: rotationAnimation.resume()
Rectangle {
id: rect
anchors.centerIn: parent
width: 40
height: 5
color: "green"
RotationAnimation {
id: rotationAnimation
target: rect
duration: (Math.random() * 2000) + 200
from: 0
to: 359
running: true
loops: Animation.Infinite
}
}
}
}
Let's compare, how grid view and table view manage the delegate lifetime.
- This exercise isn't tested, so submit it when you've done what's asked. We'll do spot-checks on the submits, so please at least try and do the exercise. Completely empty submissions will not be tolerated.
- Start from scratch, you're provided with an empty main.qml.
-
Split the window horizontally between a
GridView
andTableView
. Use Qt QuickTableView
, introduced in Qt 5.12. -
Create an empty
ListModel
. Fill the list model with 100 element. Each element has two roles: "name" and "buttonChecked". The name value could be just an index and buttonChecked should be initially false. Assign to model to both views'model
property. You may findComponent.onCompleted
signal handler very useful. After the window is created, use the handler to fill the model. -
Create two delegates: one for the grid view and another one for the table view. You can use similar delegate declaration in both cases. Add a
Row
of aRadioButton
andText
. SetRadioButton::autoExclusive
to false in both delegates. Otherwise. we cannot uncheck the checked radio button. TheText::text
property should have the "name" role, defined in the model.ButtonChecked
role is not used yet. -
Add
Component::onCompleted
signal handler to both delegates and useconsole.log
to notify about the delegate creation. Flick the grid view and table view up and down. What do you observe? How delegate creation differs between the views? - Check one radio button in the grid and one in the table. Scroll down the both views? What do you observe?
-
We need to store the radio button state into the model. That's why we have the
buttonChecked
property. First, bindRadioButton::checked
tobuttonChecked
. Now, you should see that checking a radio button in one view checks the radio button in other view, as they share the same model. - Finally, save the radio button state changes to the model. If you now scroll down and up, you should observe that the radio button state does not change or the table view does not have any re-used radio buttons in the wrong state.
Complex delegates
Keep your delegates as simple as possible. There are ways to manage complex delegates, but almost always the best solution performance and memory consumption wise is to declare simple delegates.
Why should delegates be so simple then? Problems arise when delegates contain, for example Video items all auto-playing video for some odd reason.
Item {
id: videoDelegate
Video {
id: video
width: 800
height: 600
source: model.videosrc
autoPlay: true
}
}
The target platform is low-end devices, and loading all these items at startup will take an noticeable amount of time. Some devices might not be able to play multiple video sources at the same time.
If there is a need to load resource intensive items inside a delegate, it is possible to create objects dynamically, as explained in Dynamic object creation.
QML has the Loader type, which can be used to load different parts of the UI, like complicated parts of delegates, to improve performance. The following example has a delegate which has a Component
where the video will be contained. We add a Loader
and a MouseArea
to the delegate, so when clicked the sourceComponent
property will be set to the videoComponent
. This change will trigger the Loader
to load the videoComponent
component.
Item {
id: lazyVideoDelegate
width: 200
height: 200
Component {
id: videoComponent
Video {
id: video
width: 800
height: 600
source: model.videosrc
autoPlay: true
}
}
Loader {
id: videoLoader
}
MouseArea {
anchors.fill: parent
onClicked: videoLoader.sourceComponent = videoComponent
}
}
Notice that while a Video
item declared by itself would be automatically rendered and displayed, this is not the case for the above as the item is defined inside a Component.
This will reduce the load time considerably, as the delegate will only contain Loader
and MouseArea
objects initially. Note however that we have to create a Loader
object, which is not free. It is lighter, but consumes memory anyway. The better way is to try to avoid Loaders
in delegates. The best approach would be to add an Image
which displays a thumbnail of the video or something similar.
In this part, we have concentrated on dynamic views. What are the static views then? Positioners are static views. The Repeater QML type is often used to statically create content for positioners. The idea is similar to the dynamic views. Repeater
uses delegate templates to create objects, but all objects are created synchronously.
-
You are given the
MemoryIntensiveElement
QML file. Themain.qml
empty, use to it to do the following: -
Use
MemoryIntensiveElement::nofItems
property to create e.g. 150,000 delegates. The time required for the creation depends on your computer resources, so adjusted the number so that the startup time is at least several seconds. -
Your task is simple. Reduce the startup time with asynchronous loading. You MUST not change anything in
MemoryIntensiveElement
. - Again, this exercise isn't tested, so just submit it when you think you've done what's required. We'll do spot-checks to make sure that you've at least tried.
Exhaustive reference material mentioned in this topic
http://doc.qt.io/qt-5/model-view-programming.html
https://qmlbook.github.io/en/ch06/index.html#delegate
https://www.quora.com/What-are-delegates-in-Qt
https://doc.qt.io/archives/qq/qq24-delegates.html
http://doc.qt.io/qt-5/qitemdelegate.html
https://www.ics.com/designpatterns/book/delegates.html
http://doc.qt.io/qt-5/qitemdelegate.html#details
Exercise for Part 4 - TV Guide
This is another peer-reviewed exercise. After you're done, create a repository for your solution on GitHub, and submit it to be peer-reviewed below. Please note that to get the points for this exercise, you're required to review another student's solution! You can download an example solution and do the peer-review after you've submitted your own solution.
Your task is to implement a TV Guide application. The application allows the user to browse TV programs from several channels. One source for the channel and program data is Telkussa, but obviously any other services can be used.
- The application should show TV programs of at least ten channels.
- Organise channels in a vertical list.
- Each row in the list should show at least the channel name and the current date's TV programs. Display the TV programs in a horizontal list, organised in a chronological order.
- If a TV program name is clicked on, show more detailed description of the program. QML dialogs could be used for this.
- Do not try to implement everything in a single source code file. Keep models and views in their own files.
-
Avoid hard-coded magic numbers. Make at least the view sizes scalable. The font and image sizes may be fixed.