Избегайте избыточных вызовов в QSortFilterProxyModel:: filterAcceptsRow(), если фильтр стал более узким

Есть ли какой-либо способ аннулировать фильтр в QSortFilterProxyModel, но чтобы указать, что фильтр был сужен, поэтому filterAcceptsRow() следует вызывать только в видимых в настоящее время строках?

В настоящее время Qt этого не делает. Когда я вызываю QSortFilterProxyModel::invalidateFilter(), и мой фильтр изменяется с "abcd" на "abcde", создается совершенно новое сопоставление и filterAcceptsRow() вызывается во всех исходных строках, хотя очевидно, что строки источника, которые были скрыты, далеко не останется скрытым.

Это код из источников Qt в QSortFilterProxyModelPrivate::create_mapping(), который вызывает мой переопределенный filterAcceptsRow(), и он создает совершенно новый Mapping и выполняет итерацию по всем исходным строкам:

Mapping *m = new Mapping;

int source_rows = model->rowCount(source_parent);
m->source_rows.reserve(source_rows);
for (int i = 0; i < source_rows; ++i) {
    if (q->filterAcceptsRow(i, source_parent))
        m->source_rows.append(i);
}

То, что я хочу, - это перебрать только видимые строки в сопоставлении и вызвать filterAcceptsRow() только на них. Если строка уже скрыта filterAcceptsRow() не должна вызываться на ней, потому что мы уже знаем, что она вернет false для нее (фильтр стал более строгим, он не был ослаблен).

Так как я превысил filterAcceptsRow(), Qt не может знать природу фильтра, но когда я вызываю QSortFilterProxyModel::invalidateFilter(), у меня есть информация о том, стал ли фильтр более узким, поэтому я мог передать эту информацию Qt, если он имеет способ принять его.

С другой стороны, если я изменил фильтр с abcd на abce, тогда фильтр должен быть вызван во всех исходных строках, поскольку он стал более узким.

Ответ 1

Я написал подкласс QIdentityProxyModel, в котором хранится список цепочек QSortFilterProxyModel. Он предоставляет интерфейс, похожий на QSortFilterProxyModel, и принимает логический параметр narrowedDown, который указывает, сужается ли фильтр. Так что:

  • Когда фильтр сужается, к цепочке добавляется новый QSortFilterProxyModel, а QIdentityProxyModel переключается на прокси-сервер нового фильтра в конце цепочки.
  • В противном случае It удаляет все фильтры в цепочке, создает новую цепочку с одним фильтром, который соответствует текущим критериям фильтрации. После этого QIdentityProxyModel переключается на прокси-сервер нового фильтра в цепочке.

Вот программа, которая сравнивает класс с обычным подклассом QSortFilterProxyModel:

Скриншот демонстрационной программы

#include <QtWidgets>

class FilterProxyModel : public QSortFilterProxyModel{
public:
    explicit FilterProxyModel(QObject* parent= nullptr):QSortFilterProxyModel(parent){}
    ~FilterProxyModel(){}

    //you can override filterAcceptsRow here if you want
};

//the class stores a list of chained FilterProxyModel and proxies the filter model

class NarrowableFilterProxyModel : public QIdentityProxyModel{
    Q_OBJECT
    //filtering properties of QSortFilterProxyModel
    Q_PROPERTY(QRegExp filterRegExp READ filterRegExp WRITE setFilterRegExp)
    Q_PROPERTY(int filterKeyColumn READ filterKeyColumn WRITE setFilterKeyColumn)
    Q_PROPERTY(Qt::CaseSensitivity filterCaseSensitivity READ filterCaseSensitivity WRITE setFilterCaseSensitivity)
    Q_PROPERTY(int filterRole READ filterRole WRITE setFilterRole)
public:
    explicit NarrowableFilterProxyModel(QObject* parent= nullptr):QIdentityProxyModel(parent), m_filterKeyColumn(0),
        m_filterCaseSensitivity(Qt::CaseSensitive), m_filterRole(Qt::DisplayRole), m_source(nullptr){
    }

    void setSourceModel(QAbstractItemModel* sourceModel){
        m_source= sourceModel;
        QIdentityProxyModel::setSourceModel(sourceModel);
        for(FilterProxyModel* proxyNode : m_filterProxyChain) delete proxyNode;
        m_filterProxyChain.clear();
        applyCurrentFilter();
    }

    QRegExp filterRegExp()const{return m_filterRegExp;}
    int filterKeyColumn()const{return m_filterKeyColumn;}
    Qt::CaseSensitivity filterCaseSensitivity()const{return m_filterCaseSensitivity;}
    int filterRole()const{return m_filterRole;}

    void setFilterKeyColumn(int filterKeyColumn, bool narrowedDown= false){
        m_filterKeyColumn= filterKeyColumn;
        applyCurrentFilter(narrowedDown);
    }
    void setFilterCaseSensitivity(Qt::CaseSensitivity filterCaseSensitivity, bool narrowedDown= false){
        m_filterCaseSensitivity= filterCaseSensitivity;
        applyCurrentFilter(narrowedDown);
    }
    void setFilterRole(int filterRole, bool narrowedDown= false){
        m_filterRole= filterRole;
        applyCurrentFilter(narrowedDown);
    }
    void setFilterRegExp(const QRegExp& filterRegExp, bool narrowedDown= false){
        m_filterRegExp= filterRegExp;
        applyCurrentFilter(narrowedDown);
    }
    void setFilterRegExp(const QString& filterRegExp, bool narrowedDown= false){
        m_filterRegExp.setPatternSyntax(QRegExp::RegExp);
        m_filterRegExp.setPattern(filterRegExp);
        applyCurrentFilter(narrowedDown);
    }
    void setFilterWildcard(const QString &pattern, bool narrowedDown= false){
        m_filterRegExp.setPatternSyntax(QRegExp::Wildcard);
        m_filterRegExp.setPattern(pattern);
        applyCurrentFilter(narrowedDown);
    }
    void setFilterFixedString(const QString &pattern, bool narrowedDown= false){
        m_filterRegExp.setPatternSyntax(QRegExp::FixedString);
        m_filterRegExp.setPattern(pattern);
        applyCurrentFilter(narrowedDown);
    }

private:
    void applyCurrentFilter(bool narrowDown= false){
        if(!m_source) return;
        if(narrowDown){ //if the filter is being narrowed down
            //instantiate a new filter proxy model and add it to the end of the chain
            QAbstractItemModel* proxyNodeSource= m_filterProxyChain.empty()?
                        m_source : m_filterProxyChain.last();
            FilterProxyModel* proxyNode= newProxyNode();
            proxyNode->setSourceModel(proxyNodeSource);
            QIdentityProxyModel::setSourceModel(proxyNode);
            m_filterProxyChain.append(proxyNode);
        } else { //otherwise
            //delete all filters from the current chain
            //and construct a new chain with the new filter in it
            FilterProxyModel* proxyNode= newProxyNode();
            proxyNode->setSourceModel(m_source);
            QIdentityProxyModel::setSourceModel(proxyNode);
            for(FilterProxyModel* node : m_filterProxyChain) delete node;
            m_filterProxyChain.clear();
            m_filterProxyChain.append(proxyNode);
        }
    }
    FilterProxyModel* newProxyNode(){
        //return a new child FilterModel with the current properties
        FilterProxyModel* proxyNode= new FilterProxyModel(this);
        proxyNode->setFilterRegExp(filterRegExp());
        proxyNode->setFilterKeyColumn(filterKeyColumn());
        proxyNode->setFilterCaseSensitivity(filterCaseSensitivity());
        proxyNode->setFilterRole(filterRole());
        return proxyNode;
    }
    //filtering parameters for QSortFilterProxyModel
    QRegExp m_filterRegExp;
    int m_filterKeyColumn;
    Qt::CaseSensitivity m_filterCaseSensitivity;
    int m_filterRole;

    QAbstractItemModel* m_source;
    QList<FilterProxyModel*> m_filterProxyChain;
};

//Demo program that uses the class

//used to fill the table with dummy data
std::string nextString(std::string str){
    int length= str.length();
    for(int i=length-1; i>=0; i--){
        if(str[i] < 'z'){
            str[i]++; return str;
        } else str[i]= 'a';
    }
    return std::string();
}

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    //set up GUI
    QWidget w;
    QGridLayout layout(&w);
    QLineEdit lineEditFilter;
    lineEditFilter.setPlaceholderText("filter");
    QLabel titleTable1("NarrowableFilterProxyModel:");
    QTableView tableView1;
    QLabel labelTable1;
    QLabel titleTable2("FilterProxyModel:");
    QTableView tableView2;
    QLabel labelTable2;
    layout.addWidget(&lineEditFilter,0,0,1,2);
    layout.addWidget(&titleTable1,1,0);
    layout.addWidget(&tableView1,2,0);
    layout.addWidget(&labelTable1,3,0);
    layout.addWidget(&titleTable2,1,1);
    layout.addWidget(&tableView2,2,1);
    layout.addWidget(&labelTable2,3,1);

    //set up models
    QStandardItemModel sourceModel;
    NarrowableFilterProxyModel filterModel1;;
    tableView1.setModel(&filterModel1);

    FilterProxyModel filterModel2;
    tableView2.setModel(&filterModel2);

    QObject::connect(&lineEditFilter, &QLineEdit::textChanged, [&](QString newFilter){
        QTime stopWatch;
        newFilter.prepend("^"); //match from the beginning of the name
        bool narrowedDown= newFilter.startsWith(filterModel1.filterRegExp().pattern());
        stopWatch.start();
        filterModel1.setFilterRegExp(newFilter, narrowedDown);
        labelTable1.setText(QString("took: %1 msecs").arg(stopWatch.elapsed()));
        stopWatch.start();
        filterModel2.setFilterRegExp(newFilter);
        labelTable2.setText(QString("took: %1 msecs").arg(stopWatch.elapsed()));
    });

    //fill model with strings from "aaa" to "zzz" (17576 rows)
    std::string str("aaa");
    while(!str.empty()){
        QList<QStandardItem*> row;
        row.append(new QStandardItem(QString::fromStdString(str)));
        sourceModel.appendRow(row);
        str= nextString(str);
    }
    filterModel1.setSourceModel(&sourceModel);
    filterModel2.setSourceModel(&sourceModel);

    w.show();
    return a.exec();
}

#include "main.moc"

Примечания:

  • Класс предоставляет некоторую оптимизацию только тогда, когда фильтр сужается, поскольку новый сконфигурированный фильтр в конце цепочки не нуждается в поиске по всем исходным образцовым строкам.
  • Класс зависит от пользователя, чтобы определить, сужается ли фильтр. То есть, когда пользователь передает true для аргумента narrowedDown, считается, что фильтр является особым случаем текущего фильтра (даже если это не так). В противном случае он ведет себя точно так же, как и нормальный QSortFilterProxyModel, и, возможно, с некоторыми дополнительными накладными расходами (результатом очистки старой цепи фильтра).
  • Класс может быть дополнительно оптимизирован, когда фильтр не сужается, так что он смотрит в текущей цепочке фильтров на фильтр, похожий на текущий фильтр, и сразу переключается на него (вместо удаления цепочки и начиная новый). Это может быть особенно полезно, когда пользователь удаляет некоторые символы в концевом фильтре QLineEdit (т.е. Когда фильтр изменяется с "abcd" на "abc", так как у вас уже должен быть фильтр в цепочке с "abc"). Но в настоящее время это не реализовано, так как я хочу, чтобы ответ был как можно более минимальным и ясным.

Ответ 2

Поскольку фильтры также могут быть универсальными (для пользовательской сортировки фильтра вам рекомендуется переопределить filterAcceptsRow()), ProxyModel не может знать, станет ли он уже или нет.

если вам нужно будет предоставить его прокси в качестве параметра, он сломает инкапсуляцию, потому что логика фильтра должна содержаться только внутри модели фильтра.

Вы не можете переопределить invalidateFilter, хотя, поскольку он не объявлен виртуальным. То, что вы можете сделать, это создать структуру в вашем производном прокси, где вы храните значения, которые вы в последний раз фильтровали там, и проверяете их только в том случае, когда фильтр уже сжат. Это можно сделать в filterAcceptsRow().

invalidateFilter() все равно вызовет rowCount(). Таким образом, эта функция должна иметь низкое время вызова в вашей модели, чтобы это было эффективным.

Вот какой псевдокод, как выглядел бы filterAcceptsRow():

index // some index to refer to the element;

if(!selectionNarrowed()) //need search all elements
{
    m_filteredElements.clear(); //remove all previously filtered
    if(filterApplies(getFromSource(index))) //get element from sourceModel
    {
        m_filteredElements.add(index); //if applies add to "cache"
        return true;
    }
    return false;
}

//selection has only narrowed down    
if(!filterApplies(m_filteredElements(index)) //is in "cache"?
{
    m_filteredElements.remove(index); //if not anymore: remove from cache
    return false;
}
return true;

Есть некоторые вещи, о которых нужно знать. Будьте осторожны, если вы хотите сохранить QModelIndex. Вы можете посмотреть QPersistentModelIndex.

Вам также необходимо знать об изменениях в базовой модели и подключать соответствующие слоты и аннулировать ваш "кеш" в этих случаях.

В то время как альтернативой может быть "укладка фильтра". Я вижу, что это может запутаться, когда вам действительно нужно аннулировать все фильтры.