Custom Sort/Filter Model Example

Screenshot of the Custom Sort/Filter Model Example

The QSortFilterProxyModel class provides support for sorting and filtering data passed between another model and a view.

The model transforms the structure of a source model by mapping the model indexes it supplies to new indexes, corresponding to different locations, for views to use. This approach allows a given source model to be restructured as far as views are concerned, without requiring any transformations on the underlying data and without duplicating the data in memory.

The Custom Sort/Filter Model example consists of two classes:

  • The MySortFilterProxyModel class provides a custom proxy model.

  • The Window class provides the main application window, using the custom proxy model to sort and filter a standard item model.

We will first take a look at the MySortFilterProxyModel class to see how the custom proxy model is implemented, then we will take a look at the Window class to see how the model is used. Finally we will take a quick look at the main() function.

MySortFilterProxyModel Class Definition

The MySortFilterProxyModel class inherits the QSortFilterProxyModel class.

Since QAbstractProxyModel and its subclasses are derived from QAbstractItemModel, much of the same advice about subclassing normal models also applies to proxy models.

On the other hand, it is worth noting that many of QSortFilterProxyModel's default implementations of functions are written so that they call the equivalent functions in the relevant source model. This simple proxying mechanism may need to be overridden for source models with more complex behavior. In this example we derive from the QSortFilterProxyModel class to ensure that our filter can recognize a valid range of dates, and to control the sorting behavior.

 
Sélectionnez
class MySortFilterProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT

public:
    MySortFilterProxyModel(QObject *parent = 0);

    QDate filterMinimumDate() const { return minDate; }
    void setFilterMinimumDate(const QDate &date);

    QDate filterMaximumDate() const { return maxDate; }
    void setFilterMaximumDate(const QDate &date);

protected:
    bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
    bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;

private:
    bool dateInRange(const QDate &date) const;

    QDate minDate;
    QDate maxDate;
};

We want to be able to filter our data by specifying a given period of time. For that reason, we implement the custom setFilterMinimumDate() and setFilterMaximumDate() functions as well as the corresponding filterMinimumDate() and filterMaximumDate() functions. We reimplement QSortFilterProxyModel's filterAcceptsRow() function to only accept rows with valid dates, and QSortFilterProxyModel::lessThan() to be able to sort the senders by their email addresses. Finally, we implement a dateInRange() convenience function that we will use to determine if a date is valid.

MySortFilterProxyModel Class Implementation

The MySortFilterProxyModel constructor is trivial, passing the parent parameter on to the base class constructor:

 
Sélectionnez
MySortFilterProxyModel::MySortFilterProxyModel(QObject *parent)
    : QSortFilterProxyModel(parent)
{
}

The most interesting parts of the MySortFilterProxyModel implementation are the reimplementations of QSortFilterProxyModel's filterAcceptsRow() and lessThan() functions. Let's first take a look at our customized lessThan() function.

 
Sélectionnez
bool MySortFilterProxyModel::lessThan(const QModelIndex &left,
                                      const QModelIndex &right) const
{
    QVariant leftData = sourceModel()->data(left);
    QVariant rightData = sourceModel()->data(right);

We want to sort the senders by their email addresses. The lessThan() function is used as the < operator when sorting. The default implementation handles a collection of types including QDateTime and String, but in order to be able to sort the senders by their email addresses we must first identify the address within the given string:

 
Sélectionnez
    if (leftData.type() == QVariant::DateTime) {
        return leftData.toDateTime() &lt; rightData.toDateTime();
    } else {
        static QRegExp emailPattern("[\\w\\.]*@[\\w\\.]*)");

        QString leftString = leftData.toString();
        if(left.column() == 1 &amp;&amp; emailPattern.indexIn(leftString) != -1)
            leftString = emailPattern.cap(1);

        QString rightString = rightData.toString();
        if(right.column() == 1 &amp;&amp; emailPattern.indexIn(rightString) != -1)
            rightString = emailPattern.cap(1);

        return QString::localeAwareCompare(leftString, rightString) &lt; 0;
    }
}

We use QRegExp to define a pattern for the addresses we are looking for. The QRegExp::indexIn() function attempts to find a match in the given string and returns the position of the first match, or -1 if there was no match. If the given string contains the pattern, we use QRegExp's cap() function to retrieve the actual address. The cap() function returns the text captured by the nth subexpression. The entire match has index 0 and the parenthesized subexpressions have indexes starting from 1 (excluding non-capturing parentheses).

 
Sélectionnez
bool MySortFilterProxyModel::filterAcceptsRow(int sourceRow,
        const QModelIndex &amp;sourceParent) const
{
    QModelIndex index0 = sourceModel()-&gt;index(sourceRow, 0, sourceParent);
    QModelIndex index1 = sourceModel()-&gt;index(sourceRow, 1, sourceParent);
    QModelIndex index2 = sourceModel()-&gt;index(sourceRow, 2, sourceParent);

    return (sourceModel()-&gt;data(index0).toString().contains(filterRegExp())
            || sourceModel()-&gt;data(index1).toString().contains(filterRegExp()))
            &amp;&amp; dateInRange(sourceModel()-&gt;data(index2).toDate());
}

The filterAcceptsRow() function, on the other hand, is expected to return true if the given row should be included in the model. In our example, a row is accepted if either the subject or the sender contains the given regular expression, and the date is valid.

 
Sélectionnez
bool MySortFilterProxyModel::dateInRange(const QDate &amp;date) const
{
    return (!minDate.isValid() || date &gt; minDate)
            &amp;&amp; (!maxDate.isValid() || date &lt; maxDate);
}

We use our custom dateInRange() function to determine if a date is valid.

To be able to filter our data by specifying a given period of time, we also implement functions for getting and setting the minimum and maximum dates:

 
Sélectionnez
void MySortFilterProxyModel::setFilterMinimumDate(const QDate &amp;date)
{
    minDate = date;
    invalidateFilter();
}

void MySortFilterProxyModel::setFilterMaximumDate(const QDate &amp;date)
{
    maxDate = date;
    invalidateFilter();
}

The get functions, filterMinimumDate() and filterMaximumDate(), are trivial and implemented as inline function in the header file.

This completes our custom proxy model. Let's see how we can use it in an application.

Window Class Definition

The CustomFilter class inherits QWidget, and provides this example's main application window:

 
Sélectionnez
class Window : public QWidget
{
    Q_OBJECT

public:
    Window();

    void setSourceModel(QAbstractItemModel *model);

private slots:
    void textFilterChanged();
    void dateFilterChanged();

private:
    MySortFilterProxyModel *proxyModel;

    QGroupBox *sourceGroupBox;
    QGroupBox *proxyGroupBox;
    QTreeView *sourceView;
    QTreeView *proxyView;
    QLabel *filterPatternLabel;
    QLabel *fromLabel;
    QLabel *toLabel;
    FilterWidget *filterWidget;
    QDateEdit *fromDateEdit;
    QDateEdit *toDateEdit;
};

We implement two private slots, textFilterChanged() and dateFilterChanged(), to respond to the user changing the filter pattern, case sensitivity, or any of the dates. In addition, we implement a public setSourceModel() convenience function to set up the model/ view relation.

Window Class Implementation

In this example, we have chosen to create and set the source model in the main () function (which we will come back to later). So when constructing the main application window, we assume that a source model already exists and start by creating an instance of our custom proxy model:

 
Sélectionnez
Window::Window()
{
    proxyModel = new MySortFilterProxyModel(this);

We set the dynamicSortFilter property that holds whether the proxy model is dynamically sorted and filtered. By setting this property to true, we ensure that the model is sorted and filtered whenever the contents of the source model change.

The main application window shows views of both the source model and the proxy model. The source view is quite simple:

 
Sélectionnez
sourceView = new QTreeView;
sourceView-&gt;setRootIsDecorated(false);
sourceView-&gt;setAlternatingRowColors(true);

The QTreeView class provides a default model/view implementation of a tree view. Our view implements a tree representation of items in the application's source model.

 
Sélectionnez
sourceLayout-&gt;addWidget(sourceView);
sourceGroupBox = new QGroupBox(tr("Original Model"));
sourceGroupBox-&gt;setLayout(sourceLayout);

The QTreeView class provides a default model/view implementation of a tree view; our view implements a tree representation of items in the application's source model. We add our view widget to a layout that we install on a corresponding group box.

The proxy model view, on the other hand, contains several widgets controlling the various aspects of transforming the source model's data structure:

 
Sélectionnez
filterWidget = new FilterWidget;
filterWidget-&gt;setText("Grace|Sports");
connect(filterWidget, &amp;FilterWidget::filterChanged, this, &amp;Window::textFilterChanged);

filterPatternLabel = new QLabel(tr("&amp;Filter pattern:"));
filterPatternLabel-&gt;setBuddy(filterWidget);

fromDateEdit = new QDateEdit;
fromDateEdit-&gt;setDate(QDate(1970, 01, 01));
fromLabel = new QLabel(tr("F&amp;rom:"));
fromLabel-&gt;setBuddy(fromDateEdit);

toDateEdit = new QDateEdit;
toDateEdit-&gt;setDate(QDate(2099, 12, 31));
toLabel = new QLabel(tr("&amp;To:"));
toLabel-&gt;setBuddy(toDateEdit);

connect(filterWidget, &amp;QLineEdit::textChanged,
        this, &amp;Window::textFilterChanged);
connect(fromDateEdit, &amp;QDateTimeEdit::dateChanged,
        this, &amp;Window::dateFilterChanged);
connect(toDateEdit, &amp;QDateTimeEdit::dateChanged,
this, &amp;Window::dateFilterChanged);

Note that whenever the user changes one of the filtering options, we must explicitly reapply the filter. This is done by connecting the various editors to functions that update the proxy model.

 
Sélectionnez
proxyView = new QTreeView;
proxyView-&gt;setRootIsDecorated(false);
proxyView-&gt;setAlternatingRowColors(true);
proxyView-&gt;setModel(proxyModel);
proxyView-&gt;setSortingEnabled(true);
proxyView-&gt;sortByColumn(1, Qt::AscendingOrder);

QGridLayout *proxyLayout = new QGridLayout;
proxyLayout-&gt;addWidget(proxyView, 0, 0, 1, 3);
proxyLayout-&gt;addWidget(filterPatternLabel, 1, 0);
proxyLayout-&gt;addWidget(filterWidget, 1, 1);
proxyLayout-&gt;addWidget(fromLabel, 3, 0);
proxyLayout-&gt;addWidget(fromDateEdit, 3, 1, 1, 2);
proxyLayout-&gt;addWidget(toLabel, 4, 0);
proxyLayout-&gt;addWidget(toDateEdit, 4, 1, 1, 2);

proxyGroupBox = new QGroupBox(tr("Sorted/Filtered Model"));
proxyGroupBox-&gt;setLayout(proxyLayout);

The sorting will be handled by the view. All we have to do is to enable sorting for our proxy view by setting the QTreeView::sortingEnabled property (which is false by default). Then we add all the filtering widgets and the proxy view to a layout that we install on a corresponding group box.

 
Sélectionnez
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout-&gt;addWidget(sourceGroupBox);
mainLayout-&gt;addWidget(proxyGroupBox);
setLayout(mainLayout);

setWindowTitle(tr("Custom Sort/Filter Model"));
resize(500, 450);
}

Finally, after putting our two group boxes into another layout that we install on our main application widget, we customize the application window.

As mentioned above, we create the source model in the main () function, calling the Window::setSourceModel() function to make the application use it: