How to use Qt’s QSortFilterProxyModel
By: Brad
In this tutorial I’m going to show you how to use Qt’s QSortFilterProxyModel to only present a subset of data to the presentation layer.
In Qt Quick you have a View and a Data Model where the Data Model holds the data that the View will present to the user. Your Data Model may hold data that needs to be presented in a number of different ways. The different presentation views may only need to show a subset of that data; In order to achieve this we need to present each View with a subset of the contents of the Data Model. We can do this via a Proxy Model such as the QSortFilterProxyModel.
In Qt there are classes called models which implement interfaces that allows them to satisfy all the hooks required by the framework so that views can query them for data and so they can be notified when that data changes. The base class for all models that exist in Qt is called the QAbstractItemModel, this model class has all the required methods and their base implementation satisfies the minimum required to allow views to hook up to them. Where the QAbstractItemModel is used as a base class for the sole purpose of presenting a collection of data to the presentation layer it is also inherited by a specialized class called the QAbstractProxyModel whose sole role is to allow the data to be manipulated without affecting the source data model; this way the source data model can be used by any number of different proxy modes and each proxy model can manipulate the data in different ways without effecting each other. The QAbstractProxyModel as the name implies is a general base class with just enough implementation to satisfy the requirements of allowing the data to be manipulated without effecting the source. Qt has a specialized class which inherits from the QAbstractProxyModel class called the QSortFilterProxyModel which can be used to sort and/or filter the data in the source model without effecting the source model it self. By having your view access a QSortFilterProxyModel instead of the QAbstractItemModel directly you can present your view with a subset of the data actually in the source data model.
Lets get started with some code shall we
First create a new Qt Quick Application (*.pro) project, remember this is a type of project that can have both C++ and QML. Call your new project QSortFilterProxyModel_Sample1.
First lets add a new C++ class called MyViewModel which we’ll use as our source data model. Have it extend the QStandardItemModel (which is a specialized version of the QAbstractItemModel) and define an enum that will specify some custom user roles. We’ll also have to override the virtual method roleNames so that we can define the reference keywords that the View can use to access each one of these custom roles.
#include <QStandardItemModel> #include <QObject> class MyViewModel : public QStandardItemModel { Q_OBJECT public: enum MyViewModel_Roles { MyViewModel_Roles_Display = Qt::UserRole + 1, /**< This role holds the display string for an entry in the model. */ MyViewModel_Roles_Details, /**< This role holds the details string for an entry in the model. **/ MyViewModel_Roles_KeyId, /**< This role holds the key id for an entry in the model. **/ MyViewModel_Roles_Value, /**< This role holds the value for an entry in the model. **/ }; virtual QHash<int,QByteArray> roleNames() const { QHash<int, QByteArray> roles; roles[MyViewModel_Roles_Display] = "role_display"; roles[MyViewModel_Roles_Details] = "role_details"; roles[MyViewModel_Roles_KeyId] = "role_keyid"; roles[MyViewModel_Roles_Value] = "role_value"; return roles; } }
In the constructor lets populate this model with some dummy data for use in this tutorial. Lets define 12 entries 4 will have a value assigned to the MyViewModel_Roles_Display role, 5 will have a value assigned to the MyViewModel_Roles_Details role, 3 will have a value assigned to the MyViewModel_Roles_KeyId role, and all will have a value assigned to the MyViewModel_Roles_Value role where half will have an even value and half will have an odd value.
// invisibleRootItem is part of the QStandardItemModel and is a simply way of getting the root of the model. auto root = this->invisibleRootItem(); // Create a new entry for the model QStandardItem* entry = new QStandardItem(); entry->setData("One", MyViewModel_Roles_Display ); entry->setData(0, MyViewModel_Roles_Value ); root->appendRow(entry); // Append it to the root (this will be entry #1 at index/row zero (0) ) entry = new QStandardItem(); entry->setData("One", MyViewModel_Roles_Display ); entry->setData(2, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 1 entry = new QStandardItem(); entry->setData("One", MyViewModel_Roles_Display ); entry->setData(3, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 2 entry = new QStandardItem(); entry->setData("One", MyViewModel_Roles_Display ); entry->setData(4, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 3 entry = new QStandardItem(); entry->setData("Two", MyViewModel_Roles_Details ); entry->setData(5, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 4 entry = new QStandardItem(); entry->setData("Three", MyViewModel_Roles_Details ); entry->setData(6, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 5 entry = new QStandardItem(); entry->setData("Four", MyViewModel_Roles_Details ); entry->setData(7, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 6 entry = new QStandardItem(); entry->setData("Five", MyViewModel_Roles_Details ); entry->setData(8, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 7 entry = new QStandardItem(); entry->setData("Six", MyViewModel_Roles_Details ); entry->setData(9, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 8 entry = new QStandardItem(); entry->setData("Seven", MyViewModel_Roles_KeyId ); entry->setData(10, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 9 entry = new QStandardItem(); entry->setData("Eight", MyViewModel_Roles_KeyId ); entry->setData(11, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 10 entry = new QStandardItem(); entry->setData("hello", MyViewModel_Roles_KeyId ); entry->setData(12, MyViewModel_Roles_Value ); root->appendRow(entry); // Index/Row 11
In the main.cpp file we need to instantiate an instance of this class and assign it to the root context so that the view can access it. Note to add a root context you need to include the QQmlContext header.
#include <QApplication> #include <QQmlContext> #include <QQmlApplicationEngine> #include "MyViewModel.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); // Create an instance of our ViewModel; this holds the total list of data auto vm = new MyViewModel(); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("MyModel", vm ); engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); return app.exec(); }
Now that we have a model lets create a view to show it in. In the main.qml file that gets auto generated per the Qt Quick Application project template add the following ListView component to show the data stored in the model.
Item { id: root anchors.fill: parent ListView { anchors.fill: parent model: MyModel delegate: myDelegate } Component { id: myDelegate Rectangle { anchors.left: parent.left anchors.right: parent.right height: 25 color: "green" border.color: "black" Text { text: role_value anchors.centerIn: parent } } } }
If you run this you will get the following list of all 12 entries showing the data stored in each entries MyViewModel_Roles_Value role (or attribute if you prefer to think of it that way).
Now lets say we only want to show the entries who have the Details role value set, how would we do that?
Commercial break...
Enter the QSortFilterProxyModel
In the main.cpp file lets instantiate a new instance of the QSortFilterProxyModel and set it to filter out items that don't have a MyViewModel_Roles_Details attribute with some value (if the entry does not have that attribute its value will be blank).
#include <QApplication> #include <QQmlContext> #include <QQmlApplicationEngine> #include "MyViewModel.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); // Create an instance of our ViewModel; this holds the total list of data auto vm = new MyViewModel(); auto detailsProxyModel = new QSortFilterProxyModel(); detailsProxyModel->setFilterRole( MyViewModel::MyViewModel_Roles::MyViewModel_Roles_Details); detailsProxyModel->setFilterRegExp( "^\\S+$" ); detailsProxyModel->setSourceModel( vm ); QQmlApplicationEngine engine; engine.rootContext()->setContextProperty("MyModel", vm ); engine.rootContext()->setContextProperty("DetailsRoleModel", detailsProxyModel ); engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); return app.exec(); }
Notice here that we use the QSortFilterPorxyModel method setFilterRole()
to tell the proxy what value to use when check if the entry should be included in the proxy and we use the setFilterRegExp()
method to tell the proxy how to evaluate the value. So above we are saying that for every entry found in the source model look at the value it has stored in the MyViewModel_Roles_Details role and check if it passes the given regular expression; if so then include it in the proxy model, if not then ignore it (like it doesn't exist).
To visualize it below is a diagram of what our MyViewModel class looks like when fully populated:
And here is what the detailsProxyModel instance looks like after we set its source model:
You can see here that Entries 1-4 and Entries 10-12 when the proxy model asks them for the value they have in the MyViewModel_Roles_Details role they will return a blank string (because they don't have that role defined) which does not satisfy the regular expression "^\\S+$" that we told the proxy model to use when evaluating entries. As a result the proxy model will skip them. If I were to ask the MyViewModel object for its row count it will return 12 however the detailsProxyModel will return only 5 because it is ignoring all entries which don't satisfy the set regular expression. The base implementations in the QSortFilterProxyModel for the parent(), index(), rowCount(), columnCount(), mapToSource(), mapFromSource(), data(), setData(), etc. will all ignore any entries that don't satisfy the filter. So if you ask the MyViewModel object to get you the entry at row 0 it will return Entry 1 however if you ask the detailsProxyModel for the entry at row 0 it will return you Entry 5.
Now that we have a proxy model which only presents the view with the entries who have a value in the MyViewModel_Roles_Details role we can update our view to display that value (we will no longer get blank boxes because all entries in the proxy model will have a value).
Item { id: root anchors.fill: parent ListView { id: detailsListView anchors.fill: parent model: DetailsRoleModel delegate: detailsDelegate } Component { id: detailsDelegate Rectangle { anchors.left: parent.left anchors.right: parent.right height: 25 color: "yellow" border.color: "black" Text { text: role_details anchors.centerIn: parent } } } }
That's great and all but what if my filter if more elaborate then a regular expression?
Out of the box the QSortFilterProxyModel can filter roles or columns and can evaluate the data based on a string pattern, regular expressions, or wildcard characters. If however your filter is more elaborate, perhaps you need to check two values (i.e. two different roles like show me entries who have the MyViewModel_Roles_Details role and have an even MyViewModel_Roles_Value role) your going to have to start overriding the base implementation.
Enter the filterAcceptsRow()
Virtual Method
The QSortFilterProxyModel exposes a virtual method called filterAcceptsRow()
whose base implementation checks the set column or role against the given filter (string, wildcard, regular expression). This method is a predicate which simply asks for a yes or no response of should I include this row in my proxy model; If the method returns false the proxy model will ignore the entry. You can override this method and replace it with your own implementation as long as your implementation returns a simply yes or no (include or not to include).
Commercial break...
Lets give it a shot shall we
Lets say that we now want to show a view that only has the entries which have a even number in the MyViewModel_Roles_Value role and that we can't achieve this with a regular expression (lets just say for sake of argument). To do this then we must override the filterAcceptsRow()
predicate.
First add a new class to the project called MyProxyModel_EvenEntries and have it extend the QSortFilterProxyModel. We can use all the base implementations provided to us by the QSortFilterProxyModel the only one we need to re-implement is the filterAccetpsRow()
predicate so we need to redefine it in our new class.
So you can see here its pretty straight forward, we take the source_row parameter (note this is a row relative to the source model not to the proxy model) which was passed into the predicate and use it to look up the entry in the source model. We then pull out whatever data we need to in order to satisfy our filter logic (in this case its just the MyViewModel_Roles_Value role) and evaluate it ( in this case the evaluation logic is simply to check if the value is even or not). We can insert whatever logic here we need and check as many roles or other factors as our business logic requires us to as long as we simply return a true or false response (include or not to include).
If I update my main.cpp and my QML script I will get a view which simply shows only the entries which have an even value.
There you have it, that is how to use the QSortFilterProxyModel to allow a view to only show a subset of the data contained within the root/source Data Model.
You can download sample applications which illustrate the use of a QSortFilterProxyModel here:
Sample 1: Four ListViews and Four ProxyModels (one a custom proxy model)
Sample 2: One ListView and Multiple QSortFilterProxyModels. The user can pick what data to show in the ListView.
Thank you I hope you have enjoyed and found this tutorial helpful. Feel free to leave any comments or questions you might have below and I’ll try to answer them as time permits.
Until next time think imaginatively and design creatively
Thx for the tutorial! It helped me a lot understanding how to filter through multiple roles.