Change QSortFilterProxyModel behaviour for multiple column filtering
Asked Answered
E

4

12

We have a QSortFilterProxyModel installed on a QTableView and two (or more) QLineEdit for filtering the view (based on the text of these QLineEdits)

In our view, we have a slot that tells us the string of line edits and the current column that we want. Something like this :

void onTextChange(int index, QString ntext) {
    filter.setFilterKeyColumn(index);
    filter.setFilterRegExp(QRegExp(ntext, Qt::CaseInsensitive));
}

On the first column we have names, in the second we have year of birth.

Now we enter a year for column 2 (for example, 1985). Until now, filtering is ok, but when we switch to the first line edit and enter a name (for example, John), the previous filtering based on year will reset.

How could we change this behaviour for our custom QSortFilterProxyModel?

Exploration answered 14/9, 2016 at 11:6 Comment(0)
U
10

You can subclass QSortFilterProxyModel, to make it take two separate filters (one for the name and the other for the year), and override filterAcceptsRow to return true only when both filters are satisfied.

The Qt documentation's Custom Sort/Filter Model Example shows a subclassed QSortFilterProxyModel that can take filters for dates in addition to the main string filter used for searching.

Here is a fully working example on how to make a subclassed QSortFilterProxyModel apply two separate filters for one table:

Example Screenshot

#include <QApplication>
#include <QtWidgets>

class NameYearFilterProxyModel : public QSortFilterProxyModel{
    Q_OBJECT
public:
    explicit NameYearFilterProxyModel(QObject* parent= nullptr):
        QSortFilterProxyModel(parent){
        //general parameters for the custom model
        nameRegExp.setCaseSensitivity(Qt::CaseInsensitive);
        yearRegExp.setCaseSensitivity(Qt::CaseInsensitive);
        yearRegExp.setPatternSyntax(QRegExp::RegExp);
        nameRegExp.setPatternSyntax(QRegExp::RegExp);
    }

    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const  override{
        QModelIndex nameIndex= sourceModel()->index(sourceRow, 0, sourceParent);
        QModelIndex yearIndex= sourceModel()->index(sourceRow, 1, sourceParent);

        QString name= sourceModel()->data(nameIndex).toString();
        QString year= sourceModel()->data(yearIndex).toString();

        return (name.contains(nameRegExp) && year.contains(yearRegExp));
    }
public slots:
    void setNameFilter(const QString& regExp){
        nameRegExp.setPattern(regExp);
        invalidateFilter();
    }
    void setYearFilter(const QString& regExp){
        yearRegExp.setPattern(regExp);
        invalidateFilter();
    }
private:
    QRegExp nameRegExp;
    QRegExp yearRegExp;
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    //set up GUI
    QWidget w;
    QVBoxLayout layout(&w);
    QHBoxLayout hLayout;
    QLineEdit lineEditName;
    QLineEdit lineEditYear;
    lineEditName.setPlaceholderText("name filter");
    lineEditYear.setPlaceholderText("year filter");
    lineEditYear.setValidator(new QRegExpValidator(QRegExp("[0-9]*")));
    lineEditYear.setMaxLength(4);
    hLayout.addWidget(&lineEditName);
    hLayout.addWidget(&lineEditYear);

    QTableView tableView;
    layout.addLayout(&hLayout);
    layout.addWidget(&tableView);

    //set up models
    QStandardItemModel sourceModel;
    NameYearFilterProxyModel filterModel;;
    filterModel.setSourceModel(&sourceModel);
    tableView.setModel(&filterModel);

    QObject::connect(&lineEditName, &QLineEdit::textChanged,
                     &filterModel, &NameYearFilterProxyModel::setNameFilter);
    QObject::connect(&lineEditYear, &QLineEdit::textChanged,
                     &filterModel, &NameYearFilterProxyModel::setYearFilter);

    //fill with dummy data
    QVector<QString> names{"Danny", "Christine", "Lars",
                           "Roberto", "Maria"};
    for(int i=0; i<100; i++){
        QList<QStandardItem*> row;
        row.append(new QStandardItem(names[i%names.size()]));
        row.append(new QStandardItem(QString::number((i%9)+1980)));
        sourceModel.appendRow(row);
    }
    w.show();
    return a.exec();
}

#include "main.moc"
Universality answered 14/9, 2016 at 13:6 Comment(0)
U
9

Based on this answer. Since you want to have two separate filters on your model, You can have two chained QSortFilterProxyModel(one does the filtering based on the name, and the other does the filtering based on the year using the first filtering model as the source model).

Here is a fully working example on how to have two separate filters for one table:

Example Screenshot

#include <QApplication>
#include <QtWidgets>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    //set up GUI
    QWidget w;
    QVBoxLayout layout(&w);
    QHBoxLayout hLayout;
    QLineEdit lineEditName;
    QLineEdit lineEditYear;
    lineEditName.setPlaceholderText("name filter");
    lineEditYear.setPlaceholderText("year filter");
    lineEditYear.setValidator(new QRegExpValidator(QRegExp("[0-9]*")));
    lineEditYear.setMaxLength(4);
    hLayout.addWidget(&lineEditName);
    hLayout.addWidget(&lineEditYear);

    QTableView tableView;
    layout.addLayout(&hLayout);
    layout.addWidget(&tableView);

    //set up models
    QStandardItemModel sourceModel;
    QSortFilterProxyModel yearFilterModel;
    yearFilterModel.setSourceModel(&sourceModel);
    QSortFilterProxyModel nameFilterModel;
    //nameFilterModel uses yearFilterModel as source
    nameFilterModel.setSourceModel(&yearFilterModel);
    //tableView displayes the last model in the chain nameFilterModel
    tableView.setModel(&nameFilterModel);
    nameFilterModel.setFilterKeyColumn(0);
    yearFilterModel.setFilterKeyColumn(1);
    nameFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);
    yearFilterModel.setFilterCaseSensitivity(Qt::CaseInsensitive);

    QObject::connect(&lineEditName, &QLineEdit::textChanged, &nameFilterModel,
            static_cast<void (QSortFilterProxyModel::*)(const QString&)>
            (&QSortFilterProxyModel::setFilterRegExp));
    QObject::connect(&lineEditYear, &QLineEdit::textChanged, &yearFilterModel,
            static_cast<void (QSortFilterProxyModel::*)(const QString&)>
            (&QSortFilterProxyModel::setFilterRegExp));

    //fill with dummy data
    QVector<QString> names{"Danny", "Christine", "Lars",
                           "Roberto", "Maria"};
    for(int i=0; i<100; i++){
        QList<QStandardItem*> row;
        row.append(new QStandardItem(names[i%names.size()]));
        row.append(new QStandardItem(QString::number((i%9)+1980)));
        sourceModel.appendRow(row);
    }
    w.show();
    return a.exec();
}
Universality answered 14/9, 2016 at 12:18 Comment(2)
@Mike.Is this a universal way ? in many real situation there are not only 2 column and user may filter on different column.for example if we want to add an additional column (height) to your example witch one should be the view's source model ? with ones should be child ....Exploration
@Exploration , you can chain them in any order you like, I would say (for the sake of performance) to have them chained in the order where the first filtering model would be the filter that takes most rows out, then the second, etc...Universality
I
2

If you want to connect the 2 inputs with a "and" filter you can simply layer them.

Something like this should work.

QSortFilterProxyModel namefilter;
nameFilter.setFilterKeyColumn(nameColum);
QSortFilterProxyModel yearFilter;
yearFilter.setFilterKeyColumn(yearColumn);

yearFilter.setSourceModel(model);
nameFilter.setSourceModel(&yearFilter);
view.setSource(&nameFilter);

//....


void onTextChange(int index, QString ntext)
{
    switch(index)
    {
        case yearColumn:
            yearFilter.setFilterRegExp(QRegExp(ntext, Qt::CaseInsensitive));
            break;
        case nameColum:
            namefilter.setFilterRegExp(QRegExp(ntext, Qt::CaseInsensitive));
            break;    
    }
}
Intrauterine answered 14/9, 2016 at 11:21 Comment(0)
F
2

Here is a more generic version of the QSortFilterProxyModel with a multicolumn filter implementation.

This design allows you to indicate whether the model multifilters when creating an SortFilterProxyModel object. The reason for this is so that you can add other custom behavior without having to create a separate QSortFilterProxyModel subclass.

In other words, if you create other custom behaviors by overriding QSortFilterProxyModel functions in this manner, you can pick and choose which custom sort/filter behaviors you want, and which standard sort/filter behaviors you want, for a given object.

Obviously, if you don't need or want that kind of flexibility with your subclass, you can make it your own with a few small adjustments.

Header:

class SortFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
public:
    explicit SortFilterProxyModel(bool multiFilterModel, QObject *parent = nullptr);
    void setMultiFilterRegularExpression(const int &column, const QString &pattern);
    void clearMultiFilter();

protected:
    virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
    virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;

private:
    QMap<int, QRegularExpression> m_multiFilterMap;
    bool m_multiFilterModel = false;
};

Implementation:

#include "sortfilterproxymodel.h"

//The constructor takes one additional argument (multiFilterModel) that 
//will dictate filtering behavior. If multiFilterModel is false the 
//setMultiFilterRegularExpression and clearMultifilter will do nothing.

SortFilterProxyModel::SortFilterProxyModel(bool multiFilterModel, QObject *parent) : QSortFilterProxyModel(parent)
{
    m_multiFilterModel = multiFilterModel;
}

//This loads the QMap with the column numbers and their corresponding filters.
//This member function that should be called from your main to filter model.

void SortFilterProxyModel::setMultiFilterRegularExpression(const int &column, const QString &pattern)
{
    if(!m_multiFilterModel)  //notifying that this does nothing and returning
    {
        qDebug() << "Object is not a multiFilterModel!";
        return;
    }

    QRegularExpression filter;
    filter.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
    filter.setPattern(pattern);
    m_multiFilterMap.insert(column, filter);
    invalidateFilter();  //this causes filterAcceptsRow to run
}

//This will effectively unfilter the model by making the pattern for all 
//existing regular expressions in the QMap to an empty string, and then invalidating the filter.
//This member function should be called from main to clear filter.

void SortFilterProxyModel::clearMultiFilter()
{
    if(!m_multiFilterModel)  //notifying that this does nothing and returning
    {
        qDebug() << "Object is not a multiFilterModel!";
        return;
    }

    QMap<int, QRegularExpression>::const_iterator i = m_multiFilterMap.constBegin();

    while(i != m_multiFilterMap.constEnd())
    {
        QRegularExpression blankExpression("");
        m_multiFilterMap.insert(i.key(), blankExpression);
        i++;
    }

    invalidateFilter();  //this causes filterAcceptsRow to run
}

//This checks to see if the model should be multifiltered, else it will 
//work like the standard QSortFilterProxyModel.

bool SortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
{
    if(m_multiFilterModel)
    {
        QMap<int, QRegularExpression>::const_iterator i = m_multiFilterMap.constBegin();

        while(i != m_multiFilterMap.constEnd())
        {
            QModelIndex index = sourceModel()->index(source_row, i.key(), source_parent);
            QString indexValue = sourceModel()->data(index).toString();

            if(!indexValue.contains(i.value()))
            {
                return false;  //if a value doesn't match returns false
            }
            
            i++;
         }  

        return true;  //if all column values match returns true
    }

    //This is standard QSortFilterProxyModel behavoir. It only runs if object is not multiFilterModel

    QModelIndex index = sourceModel()->index(source_row, filterKeyColumn(), source_parent);
    QString indexValue = sourceModel()->data(index).toString();

    return indexValue.contains(filterRegularExpression());
}
Fritts answered 21/1, 2023 at 22:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.