Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. General talk
  3. Blog
  4. QML SortFilterProxyModel in Qt 6.12: Composable, Range and Regular-expression filters
Qt 6.12 LTS is out! See what's new in the release blog

QML SortFilterProxyModel in Qt 6.12: Composable, Range and Regular-expression filters

Scheduled Pinned Locked Moved Unsolved Blog
3 Posts 3 Posters 114 Views 3 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • S Offline
    S Offline
    santhoshselvaraj
    wrote last edited by
    #1

    In Qt 6.10 we introduced a declarative SortFilterProxyModel for QML. It lets you sort and filter data of any QAbstractItemModel directly in QML, without changing the source model and without writing a C++ QSortFilterProxyModel subclass. If you haven’t seen it yet, the introductory post, QML Model: Sort and filter the data on the fly, walks through the basics using a process table viewer application.

    Many of you tried it and sent us feedback. Thank you! In Qt 6.12, it adds new filter types, makes FunctionFilter simpler to write, and cleans up a few names that didn’t follow Qt conventions. This post goes through the changes using the same process table viewer, so you can take the snippets from the post and update them as you read.

    Changes from Qt 6.10 to Qt 6.12

    Qt 6.10 Qt 6.12
    Assigning the source model model: processModel sourceModel: processModel (model is deprecated)
    FunctionFilter One argument of a helper RoleData component type Each role is its own typed argument: filter(cpuUsage: real)
    FunctionSorter function sort(lhs, rhs) function compare(lhs, rhs) (sort is deprecated)
    Inverting a filter invert: true inverted: true (renamed in 6.11)
    Filtering by a range of values Write a FunctionFilter New: RangeFilter
    Filtering by a regular expression Write a FunctionFilter New: RegExpFilter
    Combining filters with AND / OR Not possible (all filters are always ANDed) New: AllOfFilter and AnyOfFilter, which can be nested

    SortFilterProxyModel is still a tech preview in Qt 6.12, so its API can still change in response to your feedback.

    Revisiting the process table viewer in Qt 6.12

    As in the post, we use a processModel that exposes one row per running process with the roles pid, name, uid, state and cpuUsage, and show it in a TableView. In Qt 6.12, the basic setup looks like this:

    import QtQuick
    import QtQuick.Controls
    import QtQml.Models
    
    ApplicationWindow {
        width: 800
        height: 600
        visible: true
    
        ProcessModel { id: processModel }
    
        SortFilterProxyModel {
            id: processProxyModel
            sourceModel: processModel        // was: model: processModel
            sorters: [
                RoleSorter { roleName: "uid" }
            ]
            filters: [
                FunctionFilter {
                    function filter(cpuUsage: real): bool {
                        return cpuUsage > 0
                    }
                }
            ]
        }
    
        TableView {
            anchors.fill: parent
            model: processProxyModel
            delegate: Rectangle {
                implicitWidth: 120
                implicitHeight: 30
                border.width: 1
                Text {
                    anchors.centerIn: parent
                    text: display
                }
            }
        }
    }
    

    basic-setup.png

    Two things changed compared to Qt 6.10: the source model is set with sourceModel, and the filter function takes the role it needs directly as an argument. The following sections describe both.

    sourceModel replaces model

    SortFilterProxyModel is a proxy model, and proxy models in Qt already have a sourceModel property. Having a second property called model caused confusion, especially because views also have a model property. In Qt 6.12, use sourceModel:

    SortFilterProxyModel {
        sourceModel: processModel
    }
    

    The model property still works but is deprecated. It now also accepts only a QAbstractItemModel, which matches what was supported in practice.

    A simpler FunctionFilter

    In Qt 6.10, a FunctionFilter received the row’s data through a helper component that you had to declare yourself:

    // Qt 6.10
    FunctionFilter {
        component RoleData: QtObject {
            property real cpuUsage
        }
        function filter(data: RoleData): bool {
            return data.cpuUsage > filter.text
        }
    }
    

    In Qt 6.12, you list the roles you need as typed arguments of filter(). Each argument name must match a role name in the source model, and the proxy model passes in that role’s value for the row:

    // Qt 6.12
    FunctionFilter {
        function filter(cpuUsage: real): bool {
            return cpuUsage > Number(filter.text)
        }
    }
    

    functionfilter-search.png

    Several roles can be used at once:

    FunctionFilter {
        function filter(name: string, cpuUsage: real): bool {
            return name.startsWith("qt") && cpuUsage > 10
        }
    }
    

    functionfilter-multirole.png

    If an argument name doesn’t match any role, you get a warning that names it.

    Filtering a range of values with RangeFilter

    In the post, we used a FunctionFilter to show only processes above a CPU threshold. Range checks like this are common, so Qt 6.12 adds a dedicated RangeFilter. It keeps the rows where the value of roleName is between minimum and maximum:

    SortFilterProxyModel {
        id: processProxyModel
        sourceModel: processModel
        filters: [
            RangeFilter {
                roleName: "cpuUsage"
                minimum: 70
                maximum: 100
            }
        ]
    }
    

    rangefilter-basic.png

    Both ranges are inclusive by default. To leave the boundary value itself out, wrap it in exclusive():

    RangeFilter {
        roleName: "cpuUsage"
        minimum: exclusive(0)   // cpuUsage > 0, idle processes are hidden
    }
    

    rangefilter-exclusive.png

    Set only minimum or only maximum to get an open-ended range. Because the ranges are ordinary properties, you can bind them to UI controls, and the view updates as the user drags:

    RangeSlider {
        id: cpuRange
        from: 0; to: 100
        first.value: 10; second.value: 100
    }
    
    SortFilterProxyModel {
        sourceModel: processModel
        filters: RangeFilter {
            roleName: "cpuUsage"
            minimum: cpuRange.first.value
            maximum: cpuRange.second.value
        }
    }
    

    rangefilter-slider.png

    Filtering with RegExpFilter

    The post also showed how to filter the table with a JavaScript regular expression inside a FunctionFilter. In Qt 6.12, use RegExpFilter instead. Assign a regular expression literal to regExp:

    SortFilterProxyModel {
        sourceModel: processModel
        filters: RegExpFilter {
            roleName: "name"
            regExp: /^qt/i          // names starting with "qt", any case
        }
    }
    

    regexpfilter-literal.png

    For a search field, build the expression from the text. An empty pattern lets every row through, so the full table shows while the field is empty:

    TextField {
        id: filter
        placeholderText: qsTr("Filter process name")
    }
    
    SortFilterProxyModel {
        sourceModel: processModel
        filters: RegExpFilter {
            roleName: "name"
            regExp: new RegExp(filter.text, "i")
        }
    }
    

    regexpfilter-search.png

    Like every filter, RegExpFilter checks all columns by default. If you know which column holds the data, set column to skip the other columns.

    Combining filters with AllOfFilter and AnyOfFilter

    In Qt 6.10, every filter in the filters list had to accept a row for it to be shown. This is a logical AND, and there was no way to express “this or that”. Qt 6.12 adds two filters that group other filters:

    • AllOfFilter accepts a row when all of its child filters accept it (AND).
    • AnyOfFilter accepts a row when at least one of its child filters accepts it (OR).

    Child filters are declared directly inside the group. For example, to show processes that are either running or sleeping:

    SortFilterProxyModel {
        sourceModel: processModel
        filters: AnyOfFilter {
            ValueFilter { 
                roleName: "state"; 
                value: "Running" 
            }
            ValueFilter { 
                roleName: "state"; 
                value: "Sleeping" 
            }
        }
    }
    

    anyoffilter-state.png

    Groups can be nested to build any condition. This filter shows processes that belong to the current user or use more than 50% CPU, and whose name matches the search field:

    filters: AllOfFilter {
        RegExpFilter {
            roleName: "name"
            regExp: new RegExp(filter.text, "i")
        }
        AnyOfFilter {
            ValueFilter {
                roleName: "uid"
                value: currentUid
            }
            RangeFilter {
                roleName: "cpuUsage"
                minimum: exclusive(50)
            }
        }
    }
    

    nested-filters.png

    A group is itself a filter, so enabled and inverted apply to the whole group. This makes it easy to switch a set of conditions on and off from a single control:

    CheckBox { id: busyOrMine; text: qsTr("Only my or busy processes") }
    
    AnyOfFilter {
        enabled: busyOrMine.checked
        ValueFilter {
            roleName: "uid"
            value: currentUid
        }
        RangeFilter {
            roleName: "cpuUsage"
            minimum: exclusive(50)
        }
    }
    

    group-toggle.png

    When a group is disabled, it lets every row through, so only the rest of the filters apply.

    FunctionSorter: sort becomes compare

    A FunctionSorter method returns a negative number, zero or a positive number, so it compares two rows rather than sorting them. In Qt 6.12, the method is called compare(). The sort() name still works, but it prints a deprecation warning.

    The following sorter lists running processes first:

    FunctionSorter {
        component ProcessData: QtObject {
            property string state
        }
        function compare(lhs: ProcessData, rhs: ProcessData): int {
            const rank = s => s === "Running" ? 0 : 1
            return rank(lhs.state) - rank(rhs.state)
        }
    }
    

    Always give the arguments and the return value of compare() explicit types, as shown above. Untyped compare() functions are not supported.

    invert is now inverted

    To follow the Qt property naming convention, the invert property of all filters was renamed to inverted in Qt 6.11:

    ValueFilter {
        roleName: "uid"
        value: 0
        inverted: true      // hide processes owned by root
    }
    

    Putting it all together

    Here is the process viewer with a field to filter process name, a CPU range slider and an toggle to show busy processes. It is sorted by CPU usage and then by name:

    import QtQuick
    import QtQuick.Controls
    import QtQuick.Layouts
    import QtQml.Models
    
    ApplicationWindow {
        width: 900
        height: 600
        visible: true
        title: qsTr("Process viewer")
    
        property int currentUid: 1000   // the current user's id
    
        ProcessModel { id: processModel }
    
        SortFilterProxyModel {
            id: processProxyModel
            sourceModel: processModel
    
            filters: [
                RegExpFilter {
                    roleName: "name"
                    regExp: new RegExp(filter.text, "i")
                },
                RangeFilter {
                    roleName: "cpuUsage"
                    minimum: cpuRange.first.value
                    maximum: cpuRange.second.value
                },
                AnyOfFilter {
                    enabled: busyOrMine.checked
                    ValueFilter { 
                        roleName: "uid"; 
                        value: currentUid 
                    }
                    RangeFilter { 
                        roleName: "cpuUsage"; 
                        minimum: exclusive(50) 
                    }
                }
            ]
    
            sorters: [
                RoleSorter {
                    roleName: "cpuUsage"
                    sortOrder: Qt.DescendingOrder
                    priority: 0
                },
                StringSorter {
                    roleName: "name"
                    caseSensitivity: Qt.CaseInsensitive
                    priority: 1
                }
            ]
        }
    
        ColumnLayout {
            anchors.fill: parent
    
            RowLayout {
                TextField {
                    id: filter
                    placeholderText: qsTr("Filter process name")
                    Layout.fillWidth: true
                }
                Label { text: qsTr("CPU %") }
                RangeSlider {
                    id: cpuRange
                    from: 0; to: 100
                    first.value: 0; second.value: 100
                }
                CheckBox {
                    id: busyOrMine
                    text: qsTr("Only mine or busy")
                }
            }
    
            HorizontalHeaderView {
                syncView: tableView
                Layout.fillWidth: true
            }
    
            TableView {
                id: tableView
                Layout.fillWidth: true
                Layout.fillHeight: true
                clip: true
                model: processProxyModel
                delegate: Rectangle {
                    implicitWidth: 140
                    implicitHeight: 30
                    border.width: 1
                    Text {
                        anchors.centerIn: parent
                        text: display
                    }
                }
            }
        }
    }
    

    full-example.png

    There is no JavaScript filtering logic and no invalidate() call. Every filter is declared and bound to a control, and the table updates as the user types, drags or clicks.

    Migrating from Qt 6.10

    If your 6.10 code has… Change it to…
    model: myModel sourceModel: myModel
    invert: true inverted: true
    function filter(data: RoleData) using data.cpuUsage function filter(cpuUsage: real), and remove the RoleData component
    function sort(lhs: T, rhs: T): int function compare(lhs: T, rhs: T): int
    A FunctionFilter that checks value >= a && value <= b RangeFilter { roleName: "…"; minimum: a; maximum: b }
    A FunctionFilter that runs a regular expression RegExpFilter { roleName: "…"; regExp: /…/ }
    Several proxy models or a large function to express OR AnyOfFilter { … }, nested inside AllOfFilter if needed

    The old FunctionFilter style with a single RoleData argument is no longer supported, so update those filters when you move to Qt 6.12. Code that uses model or sort() keeps working, but prints deprecation warnings.

    Wrapping up

    With Qt 6.12, SortFilterProxyModel provides more flexibility to sort and filter data in a declarative way. The common filtering use cases, such as filtering data within a range or matching a pattern, can be configured directly through RangeFilter and RegExpFilter, and the filters can be combined with logical AND and OR through AllOfFilter and AnyOfFilter. Also, the FunctionFilter has been made simpler. SortFilterProxyModel is still a tech preview, and your feedback shapes the final API. Try the new filters in the Qt 6.12 release and tell us what works for you and what is missing, on the Qt Forum or in the Qt bug tracker.

    1 Reply Last reply
    4
    • SGaistS Offline
      SGaistS Offline
      SGaist
      Lifetime Qt Champion
      wrote last edited by
      #2

      Hi and welcome to devnet,

      Nice feature !

      Interested in AI ? www.idiap.ch
      Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

      1 Reply Last reply
      1
      • ekkescornerE Offline
        ekkescornerE Offline
        ekkescorner
        Qt Champions 2016
        wrote last edited by
        #3

        yep. really cool.
        will try later ...

        curious:
        years ago, as I switched from BlackBerry10 (Cascades/Qt 4.8) to Qt 5 (QuickControls2) I really missed the
        GroupDataModel from Cascades, where I could do things like this:

        import bb.cascades 1.2
        ListView {
            dataModel: GroupDataModel {
                sortingKeys: ["lastName", "firstName"]
                sortedAscending: true
                grouping: ItemGrouping.ByFirstChar
            }
        }
        

        great to see what's now possible with Qt QML - THX :)

        ekke ... Qt Champion 2016 | 2024 ... mobile business apps

        1 Reply Last reply
        0

        • Login

        • Login or register to search.
        • First post
          Last post
        0
        • Categories
        • Recent
        • Tags
        • Popular
        • Users
        • Groups
        • Search
        • Get Qt
        • Unsolved