QML Model: Sort and filter the data on the fly

In most data-driven applications, sorting and filtering are very common and often necessary. Qt makes this easier by providing a built-in proxy model that can handle sorting and filtering without changing the original data. This model can be attached to a view and use it within the MVC framework to display the data. Developers can also extend its functionality by inheriting from QSortFilterProxyModel and overriding its filtering and sorting APIs, allowing for fully customised behaviour. This saves developers a lot of time since they don’t need to build these features from scratch, while still giving them the flexibility to customize as needed. 

When it comes to Qt Quick, the modern UI development paradigm offers a declarative way for developers to quickly build dynamic, animated, and high-performance user interfaces. The simpler code with QML properties and JavaScript expressions also makes it readable and easier to understand. It is also to be noted that before 6.10, without the native QML proxy model, developers had to manually write sorting and filtering logic in C++, and this can result in redundant code when different sorters or filter models need to be used. The declarative way of having the model also allows developers to keep the source data manipulation part close to the UI and the actual business logic of handling the data with respect to the source model in C++.

With Qt 6.10, the QML SortFilterProxyModel is made available to sort and filter model data in the declarative way. The SortFilterProxyModel internally inherits from QAbstractProxyModel and similarly to the QSortFilterProxyModel the actual indices of the data in the source model is not altered. The source model can be set in the model attribute of the SortFilterProxyModel and the proxy model can be set in the model attribute of the views. The following example describes how to use the SortFilterProxyModel to filter the processes from the processModel that uses the CPU more than a user-specified percentage and then sort those by user-id.


	SortFilterProxyModel {
	    id: sfpmProcessModel
	    model: processModel // Source model can be specified here
	    sorters: [
		RoleSorter {
		    roleName: "uid"
		}
	    ]
	    filters: [
		FunctionFilter {
		    component RoleData: QtObject { property int cpuUsage }
		    function filter(data: RoleData) : bool {
		        return (data.cpuUsage >  parseInt(searchField.cpuUage))
		    }
		}
	    ]
	}

        TableView {
            id: processView
            anchors.fill: parent
            model: sfpmProcessModel
            clip: true
            delegate: Rectangle {
                implicitWidth: 200
                implicitHeight: 40
                border.color: "lightgray"
                border.width: 1
                Text {
                    anchors.centerIn: parent
                    text: display
                }
            }
        }

To be noted, the thoughts and works were inspired and adapted from the existing project https://github.com/oKcerG/SortFilterProxyModel.

There are certain Filters and Sorters provided by default, which can be configured accordingly to get the desired result. The individual Filter and Sorters that are configured in the list can also be prioritized resulting in order of these operations applied to the data. For instance, it can be configured to filter the data with respect to their CPU usage and then further sort it with their process id.


	SortFilterProxyModel {
	    id: sfpm
	    model: processModel
	    sorters: [
                // First sort with process id and then further sort it with name
		RoleSorter {
		    roleName: "cpu"
		    priority: 0 // Priority can be specified here
		},
		RoleSorter {
		    roleName: "pid"
		    priority: 1 // Priority can be specified here
		},
                RoleSorter {
		    roleName: "name"
		    priority: 2 // Priority can be specified here
		},
               RoleSorter {
		    roleName: "uid"
		    priority: 3 // Priority can be specified here
		},
                RoleSorter {
		    roleName: "state"
		    priority: 4 // Priority can be specified here
		}
	    ]
	    filters: [
		FunctionFilter {
		    component RoleData: QtObject { property int cpuUsage }
		    function filter(data: RoleData) : bool {
		        return (data.cpuUsage > 0)
		    }
		}
	    ]
	}

The data in the model can be filtered based on a specified column, allowing filters to be applied selectively rather than across all columns. This behavior is similar to the filterKeyColumn property and is designed to be configurable for individual filters. Note that the column index referenced here corresponds to the logical index, not the visual index.


	SortFilterProxyModel {
	    id: sfpm
	    model: processModel
	    sorters: [
		RoleSorter {
		    roleName: "name"
                    column: 1 // Column number can be specified here
		}
	    ]
	    filters: [
		FunctionFilter {
		    component RoleData: QtObject { property int cpuUsage }
		    function filter(data: RoleData) : bool {
		        return (data.cpuUsage > 1)
		    }
		}
	    ]
	}

Data filtering with regular expressions (as similar to setFilterRegularExpression) can be achieved through FunctionFilter (and sorting through the similar QML construct FunctionSorter) with the corresponding role name specified in the component to be used against.



    SortFilterProxyModel {
        id: sfpmProcessModel
        model: processModel
        sorters: [
            RoleSorter {
                roleName: "name"
                column: 1
            }
        ]
        filters: [
            FunctionFilter {
                id: filter
                component RoleData: QtObject {
                    property int pid
                    property string name
                    property string user
                    property string state
                    property int cpu
                }
                // JS regular expression comparision can be made as mentioned below
                property var regExp: new RegExp(searchField.text, "i")
                onRegExpChanged: invalidate()
                function filter(data: RoleData) : bool {
                    if (searchField.text === "")
                        return true
                    switch (filterCb.currentIndex) {
                    case 0: return (data.pid == parseInt(searchField.text))
                    case 1: return regExp.test(data.name)
                    case 2: return regExp.test(data.user)
                    case 3: return regExp.test(data.state)
                    case 4: return (data.cpu >= parseInt(searchField.text))
                    default: true
                    }
                }
            }
        ]
    }

The documentation to the SortFilterProxyModel contains detailed description about the corresponding filter and sorter properties and information related to it. The SortFilterProxyModel internally uses and adopts much of the same algorithms as QSortFilterProxyModel for data manipulation. The feature is in tech preview as of 6.10 and so please expect the change in APIs with the feedback.

Future scope

For future version, we are considering supporting nested filters and sorters.

The model attribute currently supports only the QAbstractItemModel. Still, the actual type of the model within SortFilterProxyModel had been chosen as QVariant, which, in the future, can be extended to support JavaScript arrays as well.

Also, defining the custom filters and sorters was not possible as of now, as their bases are internal, and they can be exposed in the future to accommodate this requirement.

Thanks for reading through this blog and your feedback on improvisation of this feature would really be helpful.


Blog Topics:

Comments