Lightweight C++ Dynamic QSortFilterProxyModel (Model/View Programming)
While researching how to setup models and connect them to views I came across an interesting issue. Nobody could provide me with a good solution to setup dynamic filters for filtering information in a CSV file. So... I made one myself! What better way to re-introduce myself to the world of C++!
Important terms used in this article:
Features of tool:
Dynamic number of filter edits based on CSV file columns
Filter through all columns
Save filtered selection into a new file
Why learn Model/View programming?:
Up until now I have been using PyQt extensively, and when creating tables to display information it has been entirely QTableWidgets simply for their ease of use. The concept of Model/View programming has been alien to me and I wanted to learn.
QTableWidgets are totally fine to use and are very easy to setup but when you want to filter information or simply reload your entire table this involves syncing two sections of data, your initial data and all the QTableWidgetItems you have to create.
Using Model/View programming techniques with Qt we can alleviate this pain and also cut the level of code down by a huge margin as we are no longer syncing two sections of data.
The data you have, perhaps in a multi-vector is setup inside a model, you then tell the view to use this model. When you filter, all you are doing is telling the model to change which rows or columns are active. This updates the view allowing your data to remain unchanged!
So we set our model up with our data, tell the view to use this model. And boom, it shows all of our info for us keeping view and data separate!
I won't re-write the explanations of what Model/View programming is as there are already excellent tutorials and explanations on the Qt site:
The solution to dynamic filters:
The problem I had was there was no standard solution to be able to open a CSV file of any size and be able to dynamically filter through each column using LineEdits.
The example I worked from was this Stackoverflow post which illustrates how to setup a QSortFilterProxyModel (our filter and sort model) for 2 columns. The solution proposed in the article is to subclass QSortFilterProxyModel and cutomise it to handle your columns.
Pay attention to this section:
The QSortFilterProxyModel requires you to create and connect a QRegularExpression (QRegExp) so that the correct string input is mapped to the correct column in your model, we connect the QRegExp to a LineEdit for filtering.
So for column two we must setup a QRegExp and a LineEdit and tell the Model that these 2 are connected. We do this like so:
What is wrong with the proposed solution on Stackoverflow?:
There is no way you can set this up for a variable amount of columns.
To work around this what we can do is to store the QRegExp and the LineEdits in an ordered struct inside our subclassed QSortFilterProxyModel. The above class will now look more like this:
Our class implements a struct to keep track of our LineEdits and equivalent QRegExp
The setupFilters function fills a vector of structs to store our LineEdits and QRegExp.
The function takes a list of headers to iterate through, this list represents each of our columns. e.g. [First Name, Last Name, Address, Phone, etc.]
We set our LineEdit with an object name, this allows us to query it later when we type into it. This is a very important part of the code.
How do we tell our Model which LineEdit has been triggered?:
This is where our object name comes into play.
When you change the text in a LineEdit we listen for the textChanged signal.
In the Stackoverflow example each QRegExp is connected to each individual LineEdit by hand and the filter is invalidated telling the model that something has changed and we need to update the view.
For our program we need to be able to handle a variable number of LineEdits to filter information in the table.
When we setup our table in our main class (Line:128, function: setupFilterSignals):
We connect the setFilter function to each line edit in our multi_array_table cpp file.
QObject::connect(filterModel->filters[i].filterEdit, SIGNAL(textChanged(const QString &)), filterModel, SLOT(setFilter(const QString &)));
Setting the filters dynamically:
The function below is how we find out which LineEdit and QRegExp to invalidate and filter with.
In our setFilter function we do not know which LineEdit has sent the signal so we use the sender() function which returns the object that has sent the signal. We can then get the object name from this.
We iterate through our vector of structs and check each LineEdit name. If we get a match we set our QRegExp and invalidateFilter() thus updating our model.
When does the filtering actually happen and how?
In our CustomProxyModel there is a fucntion that we have customised called filterAcceptsRow().
This function is how the filtering is done. It will return true if the contents of QRegExp is in the row.
In our custom function we iterate over each of our QRegExp and check if each of our row has the expression in it. We return the concatenated boolean result.
This is the end of the article and I hope that you have learned something new and cool about Qt programming. I have glossed over how to setup a dynamic filtering table for any number of columns based on a CSV input. The rest of the code is yours to look into.
Feel free to dig into the code and play around with it. Make sure you read around when you are digging into the techniques used here. Enjoy!