Избегайте избыточных вызовов 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, тогда фильтр должен быть вызван во всех исходных строках, так как он стал строго уже.

2 ответа

Решение

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

  • Когда фильтр сужается, новый QSortFilterProxyModel присоединяется к цепочке, а QIdentityProxyModel переключает на прокси новый фильтр в конце цепочки.
  • В противном случае Он удаляет все фильтры в цепочке, создает новую цепочку с одним фильтром, который соответствует текущим критериям фильтрации. После этого 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"). Но в настоящее время это не реализовано, так как я хочу, чтобы ответ был минимальным и четким, насколько это возможно.

Поскольку фильтры также могут быть общими (для пользовательской сортировки фильтров рекомендуется переопределить 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.

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

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

Другие вопросы по тегам