15 Jan 2019

Using DelegateModel in QML for Sorting and Filtering

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.

Where We Want to Get to: Model Sorting and Filtering in QML

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:

Filtering and Sorting Done in QML

Quick Walk Through the App’s Code

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:

  1. We assign a comparison function to the 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.
  2. Similarly, the function assigned to filterAcceptsItem gets and item and returns true if the item shall be visible or false otherwise.
  3. The 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).
  4. Finally, we define a 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…

  1. One button to toggle between odd and even numbers.
  2. A second button is used to toggle between lexical and natural ordering of entries.
  3. Finally, there is a slider which we can use to change the amount of items in the source model.

So far, so good. Next, let’s have a look at how the SortFilterModel is implemented.

The SortFilterModel Component

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

  1. It removes all items from the 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.
  2. Second, we gather all items that are visible. For this, we iterate over all items in the 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.
  3. After we gathered all visible items, we call the sort method on the JavaScript array, passing in a custom sort function, which is based on our lessThan function.
  4. In the last step, we iterate over the intermediate list of items and put them into the 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!

But… Can It Deal With Something Else Like Numbers, Too?

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

Our `SortFilterModel` used with an `ItemModel` as source

Some Hints on Usage

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.

Thank You For Reading
Martin Hoeher

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.

Comments
Your comment has been filed

We'll review it and it will appear here as soon as we're done.

Sorry, something went wrong...

Your comment could not be posted.

Regarding your personal information...
  • The name you enter will be shown next to your comment. You may enter your real name or whatever you like.
  • Your e-mail will be used to show an image representing you next to your comment. We do not store nor show your address somewhere. Instead, an ID will be calculated from it and stored. This ID is then used to retrieve an image from Gravatar.