Diego Thomé
pretty good you are a genius, tanks <3
When developing with the Qt framework, you might already have stumbled over its model view framework, consisting of classes like QAbstractItemModel, QTreeView and QSortFilterProxyModel.
In particular, QSortFilterProxyModel
is interesting: In the simplest case, you create a model which you plug into a view, which displays the exposed items. The QSortFilterProxyModel
in turn can be plugged in between the source model and the view. With its help, we can easily filter the list of items or reorder them. We can even stack such proxy models, which allows to combine different sorting and filtering criteria as needed.
When it comes to QML, things are not quite as straightforward. Of course, you can continue to use the C++ model/view classes, including proxy models. However, if you have models which are defined solely on QML side, or you want to make use of the possibility of QML to use other kinds of models - in the simplest case a simple integer number - you need another solution.
Here, the QML type DelegateModel proofs to be useful. Although it is (in my opinion) not documented as good as it could be, we will use it to implement sorting and filtering of an arbitrary source model in QML. However, a warning ahead: If you have larger data sets, consider sticking with the C++ model classes instead! Only use this approach when you know that your data sets are relatively small, so the impact of doing the filtering in the QML layer does not impact your app too much.
Let’s start with what we want to archive in the end: A GUI which has a simple model (a number which the user can change) and which we filter and sort entirely in QML. The code for such an application could roughly look like this:
import QtQuick 2.9
import QtQuick.Window 2.3
import QtQuick.Controls 2.4
import QtQml.Models 2.3
import QtQuick.Layouts 1.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
SortFilterModel {
id: delegateModel
lessThan: function(left, right) {
if (view.sortLexically) {
return left.modelData.toString() < right.modelData.toString();
} else {
return left.modelData < right.modelData;
}
}
filterAcceptsItem: function(item) {
return item.modelData % 2 == view.remainder;
}
model: numberOfItems.value
delegate: Text {
id: item
text: qsTr("This is item #%1").arg(modelData)
}
}
ListView {
id: view
property int remainder: 0
property bool sortLexically: true
anchors {
left: parent.left
right: parent.right
top: parent.top
bottom: controls.top
}
model: delegateModel
onRemainderChanged: delegateModel.update()
onSortLexicallyChanged: delegateModel.update()
}
RowLayout {
id: controls
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Button {
text: qsTr("Toggle odd/even")
onClicked: view.remainder = (view.remainder + 1) % 2;
}
Button {
text: qsTr("Toggle sort")
onClicked: view.sortLexically = !view.sortLexically
}
Slider {
id: numberOfItems
from: 10
to: 1000
value: 100
Layout.fillWidth: true
}
}
}
And this is how it looks like when rendered:
Let’s quickly jump through the app code. First, we instantiate out SortFilterModel
(we are going to look at it’s code later):
SortFilterModel {
id: delegateModel
lessThan: function(left, right) {
if (view.sortLexically) {
return left.modelData.toString() < right.modelData.toString();
} else {
return left.modelData < right.modelData;
}
}
filterAcceptsItem: function(item) {
return item.modelData % 2 == view.remainder;
}
model: numberOfItems.value
delegate: Text {
id: item
text: qsTr("This is item #%1").arg(modelData)
}
}
It is basically what would you expect:
lessThan
property. This function is used to compare two entries of the model and returns true of the left one is less than the right one, otherwise false. The parameters left
and right
are containers which have the same properties that otherwise would be exposed to the delegate of a view when used with the same model as source.filterAcceptsItem
gets and item
and returns true if the item shall be visible or false otherwise.model
property is set to the source model we want to use. We can set it to any model we need, be it a QAbstractItemModel
, a list or even a simple number. In our case, we bind it to a number (which is coming from a slider component defined later).delegate
. This is what is potentially new and unexpected, as usually we set delegates in Views. However, as we use a DelegateModel
as a base, we set the delegate in the model.Next, let’s continue with the view:
ListView {
id: view
property int remainder: 0
property bool sortLexically: true
anchors {
left: parent.left
right: parent.right
top: parent.top
bottom: controls.top
}
model: delegateModel
onRemainderChanged: delegateModel.update()
onSortLexicallyChanged: delegateModel.update()
}
Nothing special. We anchor the view and set it’s model
property to our SortFilterModel
. We also define two helper properties (remainder
and sortLexically
) which we use to control sorting and filtering. Note that we call the update()
method of our SortFilterModel
when these two properties change to apply the changed sorting and filtering. Also note that we do not set a delegate
in the view, as this is coming from our source model.
Finally, we add some controls to change sorting, filtering and also the amount of items in the source model:
RowLayout {
id: controls
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
Button {
text: qsTr("Toggle odd/even")
onClicked: view.remainder = (view.remainder + 1) % 2;
}
Button {
text: qsTr("Toggle sort")
onClicked: view.sortLexically = !view.sortLexically
}
Slider {
id: numberOfItems
from: 10
to: 1000
value: 100
Layout.fillWidth: true
}
}
We have…
So far, so good. Next, let’s have a look at how the SortFilterModel
is implemented.
SortFilterModel
Componentimport QtQuick 2.9
import QtQml.Models 2.3
DelegateModel {
id: delegateModel
property var lessThan: function(left, right) { return true; }
property var filterAcceptsItem: function(item) { return true; }
function update() {
if (items.count > 0) {
items.setGroups(0, items.count, "items");
}
// Step 1: Filter items
var visible = [];
for (var i = 0; i < items.count; ++i) {
var item = items.get(i);
if (filterAcceptsItem(item.model)) {
visible.push(item);
}
}
// Step 2: Sort the list of visible items
visible.sort(function(a, b) {
return lessThan(a.model, b.model) ? -1 : 1;
});
// Step 3: Add all items to the visible group:
for (i = 0; i < visible.length; ++i) {
item = visible[i];
item.inVisible = true;
if (item.visibleIndex !== i) {
visibleItems.move(item.visibleIndex, i, 1);
}
}
}
items.onChanged: update()
onLessThanChanged: update()
onFilterAcceptsItemChanged: update()
groups: DelegateModelGroup {
id: visibleItems
name: "visible"
includeByDefault: false
}
filterOnGroup: "visible"
}
As announced, the component is based on QML’s DelegateModel
. We actually just need a few things on top:
property var lessThan: function(left, right) { return true; }
property var filterAcceptsItem: function(item) { return true; }
We define two properties lessThan
and filterAcceptsItem
which shall be set to functions which are then used to sort and filter our source model. The defaults are set such that the SortFilterModel
basically does an identity mapping, not changing the sorting nor visibility of the items from the source model.
groups: DelegateModelGroup {
id: visibleItems
name: "visible"
includeByDefault: false
}
filterOnGroup: "visible"
This is something you have to get used to: A DelegateModel
uses different so called groups to do its job. It has a default, built in group called items
, which is always there. By assigning a single or a list of DelegateModelGroup
instances we can add more groups. For our model, we add just one new group visible
, into which we put the items that are visible later on. By setting it’s includeByDefault
property to false, items added to the source model are not automatically added to the group. Lastly, we set the filterOnGroup
property to visible
. This way, only items that are in the visible
group are shown.
function update() {
if (items.count > 0) {
items.setGroups(0, items.count, "items");
}
// Step 1: Filter items
var visible = [];
for (var i = 0; i < items.count; ++i) {
var item = items.get(i);
if (filterAcceptsItem(item.model)) {
visible.push(item);
}
}
// Step 2: Sort the list of visible items
visible.sort(function(a, b) {
return lessThan(a.model, b.model) ? -1 : 1;
});
// Step 3: Add all items to the visible group:
for (i = 0; i < visible.length; ++i) {
item = visible[i];
item.inVisible = true;
if (item.visibleIndex !== i) {
visibleItems.move(item.visibleIndex, i, 1);
}
}
}
This function is basically the heart of the implementation. It is responsible for filtering and sorting the source model. For this, the model does the following:
visible
group, by calling the setGroups
method of the items group. It can be used to change the group memberships of several items at once. In our case, we set the group membership of all items in the items
group to only ["item"]
only.items
group. Each entry has a member model
, which contains a map of all properties from the source model - this is basically what would also be visible in a delegate. If the filterAcceptsItem
function returns true, we put the item into an intermediate JavaScript array.sort
method on the JavaScript array, passing in a custom sort function, which is based on our lessThan
function.visible
group. This is done by setting each item’s inVisible
property to true. In addition, we use the mode method of the visible group to move the item to the position where it belongs to.That’s it!
Sure 😎
The great thing about using DelegateModel
as base for our SortFilterModel
, we can deal with everything: The DelegateModel
just passes through the properties of the items in the source model. In the simplest case where you have a number as model, we only have the modelData
attribute, which is the index of the item in the source model. If we use something else, the attributes are transparently made available, too. Here’s an example using a ListModel
as base:
import QtQuick 2.9
import QtQuick.Window 2.3
import QtQuick.Controls 2.4
import QtQml.Models 2.3
import QtQuick.Layouts 1.3
Window {
visible: true
width: 640
height: 480
title: qsTr("Hello World")
ListModel {
id: nameModel
ListElement { name: "Alice"; team: "Crypto" }
ListElement { name: "Bob"; team: "Crypto" }
ListElement { name: "Jane"; team: "QA" }
ListElement { name: "Victor"; team: "QA" }
ListElement { name: "Wendy"; team: "Graphics" }
}
RowLayout {
id: controls
anchors {
left: parent.left
top: parent.top
right: parent.right
}
TextField {
id: nameFilter
placeholderText: qsTr("Search by name...")
Layout.fillWidth: true
onTextChanged: sortFilterModel.update()
}
RadioButton {
id: sortByName
checked: true
text: qsTr("Sort by name")
onCheckedChanged: sortFilterModel.update()
}
RadioButton {
text: qsTr("Sort by team")
}
}
SortFilterModel {
id: sortFilterModel
model: nameModel
filterAcceptsItem: function(item) {
return item.name.includes(nameFilter.text)
}
lessThan: function(left, right) {
if (sortByName.checked) {
var leftVal = left.name;
var rightVal = right.name;
} else {
leftVal = left.team;
rightVal = right.team;
}
return leftVal < rightVal ? -1 : 1;
}
delegate: Text {
text: name + " (" + team + ")"
}
}
ListView {
anchors {
left: parent.left
top: controls.bottom
right: parent.right
bottom: parent.bottom
}
model: sortFilterModel
}
}
As already pointed out: Do not use this approach for large lists of items. As sorting and filtering is done in QML/JavaScript, this could be a performance killer for your app.
Also note that with the SortFilterModel
as it is implemented, you also get no “dynamic” filtering. When e.g. sorting or filtering criteria change, you have to manually call the update
method.
I am a software/firmware developer, working in Dresden, Germany. In my free time, I spent some time on developing apps, presenting some interesting findings here in my blog.
pretty good you are a genius, tanks <3
Thanks for the good tutorial and your neat component!
thank you for this idea, i will check DelegateModel
Thanks for the smart Component. You should just correct the lessThan function that should return 0 in case of equality. Something like this, but not optimized:
visible.sort(function(a, b) {
return lessThan(a.model, b.model) ? -1 : (lessthan(b.model,a.model ? 1 : 0))
});
Thanks Daes for your comment.
You are right in that with the current approach “equal” list elements will get reordered. However, this was perfectly fine for the use case I had 😉
If someone needs this feature, instead of implementing a lessThan()
function, a compare()
one could be implemented, which returns -1, 0 or 1 as desired. This function would just be used like visible.sort(compare)
.
Hi, thanks for this nice explanation, it helps make sense of the DelegateModel documentation. What would you consider to be a large list; 10, 100, 500, 1000, 10000? I’m looking at a list of items that can’t credibly be > 1000 but with several filter terms. I was hoping to avoid QFilterProxyModel in C++, I’d prefer to keep the flexibility of a QML implementation.
As so often: It depends on your concrete application 😉
In the examples above, the model had - at least on my machine - no issues with a list of 1000 entries. I guess also 10000 is not a real issue (you can give it a try: Increase the max value for the slider in the example above and see how far you can get).
However, the example also has only trivial logic for the filtering. If your filter is very compute intensive, it might be worth factoring this part out to C++. However, even then you could maybe live without having to implement the complete model on C++ side but just implement your filter function there and make it available to QML.
In general, if you prefer to do as much on the QML side as possible, you can start by using the built in models and only “migrate” functionality to C++ if you experience performance issues later on. If you use a ListModel
as base, it should be possible to easily do such a move without having to rewrite too much of your existing QML code.
Thanks, this is awesome!