Home · Examples 


Custom Sort/Filter Model Example

Code:

The Custom Sort/Filter Model example illustrates how to subclass QSortFilterProxyModel to perform advanced sorting and filtering.

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:

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 CustomFilter class to see how the model is used.

MySortFilterProxyModel Class Implementation

The MySortFilterProxyModel class extends the
QSortFilterProxyModel class:
    private class MySortFilterProxyModel extends QSortFilterProxyModel {
        private QDateTime minDate = new QDateTime();
        private QDateTime maxDate = new QDateTime();

        private MySortFilterProxyModel(QObject parent) {
            super(parent);
        }
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 methods are written so that they call the equivalent methods 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.

        private void setFilterMinimumDate(QDate date) {
            minDate = new QDateTime(date);
            invalidateFilter();
        }

        private void setFilterMaximumDate(QDate date) {

            maxDate = new QDateTime(date);
            invalidateFilter();
        }
We want to be able to filter our data by specifying a given period of time. For that reason, we implement custom setFilterMinimumDate() and setFilterMaximumDate() methods to set the corresponding variables. The QSortFilterProxyModel.filterChanged() method updates the proxy model's mapping by reapplying the filter. Note that QSortFilterProxyModel also provides the clear() method that removes all mapping, forcing the model to update the sorting as well as the data filtering.

In addition, we reimplement QSortFilterProxyModel's filterAcceptsRow() method to only accept rows with valid dates:

        protected boolean filterAcceptsRow(int sourceRow,
                                           QModelIndex sourceParent) {
            QModelIndex index0;
            QModelIndex index1;
            QModelIndex index2;

            index0 = sourceModel().index(sourceRow, 0, sourceParent);
            index1 = sourceModel().index(sourceRow, 1, sourceParent);
            index2 = sourceModel().index(sourceRow, 2, sourceParent);

            QRegExp filter = filterRegExp();
            QAbstractItemModel model = sourceModel();
            boolean matchFound;

            matchFound = filter.indexIn(model.data(index0).toString()) != -1
                         || filter.indexIn(model.data(index1).toString()) != -1;

            return matchFound && dateInRange((QDateTime) (model.data(index2)));
        }

        private boolean dateInRange(QDateTime date) {
            return (minDate.compareTo(date) < 0 && maxDate.compareTo(date) > 0);
        }
The filterAcceptsRow() method 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.
        protected boolean lessThan(QModelIndex left, QModelIndex right) {

            boolean result = false;
            Object leftData = sourceModel().data(left);
            Object rightData = sourceModel().data(right);
Finally, we want to be able to sort the senders by their email adresses. For that reason we must reimplement the QSortFilterProxyModel.lessThan() method. This method 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 adresses we must first identify the adress within the given string:
            if (leftData instanceof QDateTime
                && rightData instanceof QDateTime) {

                QDateTime leftDate = (QDateTime) leftData;
                QDateTime rightDate = (QDateTime) rightData;

                result = leftDate.compareTo(rightDate) < 0;
            } else {

                QRegExp emailPattern = new QRegExp("([\\w\\.]*@[\\w\\.]*)");

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

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

                result = leftString.compareTo(rightString) < 0;
            }
            return result;
        }

    }
We use QRegExp to define a pattern for the adresses we are looking for. The QRegExp.indexIn() method 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() method to retrieve the actual adress. The cap() method 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).

The reimplementation of the lessThan() method completes our custom proxy model. Let's see how we can use it in an application.

CustomFilter Class Implementation

The CustomFilter class extends QWidget, and provides the main application window:
public class CustomFilter extends QWidget {
...
    public CustomFilter() {
        QStandardItemModel model = createMailModel(this);
In the constructor, we first create our source model by calling the createMailModel() method. The QStandardItemModel class provides a generic model for storing custom data, and can be used as a repository for standard Qt data types.
        proxyModel = new MySortFilterProxyModel(this);
        proxyModel.setSourceModel(model);
        proxyModel.setDynamicSortFilter(true);
Then we create a proxy model. By calling the QSortFilterProxyModel.setSourceModel() method, we make the proxy model process the data in our mail model. We also 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:

        sourceView = new QTreeView();
        sourceView.setRootIsDecorated(false);
        sourceView.setAlternatingRowColors(true);
        sourceView.setModel(model);

        QHBoxLayout sourceLayout = new QHBoxLayout();
        sourceLayout.addWidget(sourceView);

        sourceGroupBox = new QGroupBox(tr("Original Model"));
        sourceGroupBox.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:

        filterPatternLineEdit = new QLineEdit("Grace|Sports");
        filterPatternLabel = new QLabel(tr("&Filter pattern:"));
        filterPatternLabel.setBuddy(filterPatternLineEdit);

        filterSyntaxComboBox = new QComboBox();
        filterSyntaxComboBox.addItem(tr("Regular expression"),
                                     QRegExp.PatternSyntax.RegExp);
        filterSyntaxComboBox.addItem(tr("Wildcard"),
                                     QRegExp.PatternSyntax.Wildcard);
        filterSyntaxComboBox.addItem(tr("Fixed string"),
                                     QRegExp.PatternSyntax.FixedString);

        filterCaseSensitivityCheckBox = new QCheckBox(
                tr("Case sensitive filter"));
        filterCaseSensitivityCheckBox.setChecked(true);

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

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

        filterPatternLineEdit.textChanged.connect(this, "textFilterChanged()");
        filterSyntaxComboBox.currentIndexChanged.connect(this,
                                                         "textFilterChanged()");
        filterCaseSensitivityCheckBox.toggled.connect(this,
                                                      "textFilterChanged()");
        fromDateEdit.dateChanged.connect(this, "dateFilterChanged()");
        toDateEdit.dateChanged.connect(this, "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 methods that update the proxy model.
        proxyView = new QTreeView();
        proxyView.setRootIsDecorated(false);
        proxyView.setAlternatingRowColors(true);
        proxyView.setModel(proxyModel);
        proxyView.setSortingEnabled(true);
        proxyView.sortByColumn(1, Qt.SortOrder.AscendingOrder);

        QGridLayout proxyLayout = new QGridLayout();
        proxyLayout.addWidget(proxyView, 0, 0, 1, 3);
        proxyLayout.addWidget(filterPatternLabel, 1, 0);
        proxyLayout.addWidget(filterPatternLineEdit, 1, 1);
        proxyLayout.addWidget(filterSyntaxComboBox, 1, 2);
        proxyLayout.addWidget(filterCaseSensitivityCheckBox, 2, 0, 1, 3);
        proxyLayout.addWidget(fromLabel, 3, 0);
        proxyLayout.addWidget(fromDateEdit, 3, 1, 1, 2);
        proxyLayout.addWidget(toLabel, 4, 0);
        proxyLayout.addWidget(toDateEdit, 4, 1, 1, 2);

        proxyGroupBox = new QGroupBox(tr("Sorted/Filtered Model"));
        proxyGroupBox.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.
        QVBoxLayout mainLayout = new QVBoxLayout();
        mainLayout.addWidget(sourceGroupBox);
        mainLayout.addWidget(proxyGroupBox);
        setLayout(mainLayout);

        setWindowTitle(tr("Custom Sort/Filter Model"));
        setWindowIcon(new QIcon("classpath:com/trolltech/images/qt-logo.png"));
        resize(500, 450);

        textFilterChanged();
        dateFilterChanged();
    }
After putting our two group boxes into another layout that we install on our main application widget, we customize the application window. Finally, we call the textFilterChanged() and dateFilterChanged() methods to update the proxy model according to the filtering widgets's initial values.
    private void textFilterChanged() {
        QRegExp.PatternSyntax syntax;
        int index = filterSyntaxComboBox.currentIndex();
        syntax = (QRegExp.PatternSyntax) filterSyntaxComboBox.itemData(index);

        Qt.CaseSensitivity caseSensitivity;
        if (filterCaseSensitivityCheckBox.isChecked())
            caseSensitivity = Qt.CaseSensitivity.CaseSensitive;
        else
            caseSensitivity = Qt.CaseSensitivity.CaseInsensitive;

        QRegExp regExp = new QRegExp(filterPatternLineEdit.text(),
                                     caseSensitivity, syntax);
        proxyModel.setFilterRegExp(regExp);
    }
The textFilterChanged() method is called whenever the user changes the filter pattern or the case sensitivity.

We first retrieve the preferred syntax (the QRegExp.PatternSyntax enum is used to interpret the meaning of the given pattern), then we determine the preferred case sensitivity. Based on these preferences and the current filter pattern, we set the proxy model's filterRegExp property. The filterRegExp property holds the regular expression used to filter the contents of the source model; calling QSortFilterProxyModel's setFilterRegExp() method also updates the model.

    private void dateFilterChanged() {
        proxyModel.setFilterMinimumDate(fromDateEdit.date());
        proxyModel.setFilterMaximumDate(toDateEdit.date());
    }
The dateFilterChanged() method is called whenever the user modifies the range of valid dates. We retrieve the new dates from the user interface, and call the corresponding methods (provided by our custom proxy model) to set the proxy model's minimum and maximum dates. As we explained above, calling these methods also updates the model.
    private QStandardItemModel createMailModel(QObject parent) {
        QStandardItemModel model = new QStandardItemModel(0, 3, parent);

        model.setHeaderData(0, Qt.Orientation.Horizontal, tr("Subject"));
        model.setHeaderData(1, Qt.Orientation.Horizontal, tr("Sender"));
        model.setHeaderData(2, Qt.Orientation.Horizontal, tr("Date"));

        addMail(model, "Happy New Year!", "Grace K. <grace@software-inc.com>",
                new QDateTime(new QDate(2006, 12, 31), new QTime(17, 03)));
        addMail(model, "Radically new concept",
                "Grace K. <grace@software-inc.com>",
                new QDateTime(new QDate(2006, 12, 22), new QTime(9, 44)));
        addMail(model, "Accounts", "pascale@nospam.com",
                new QDateTime(new QDate(2006, 12, 31), new QTime(12, 50)));
        addMail(model, "Expenses", "Joe Bloggs <joe@bloggs.com>",
                new QDateTime(new QDate(2006, 12, 25), new QTime(11, 39)));
        addMail(model, "Re: Expenses", "Andy <andy@nospam.com>",
                new QDateTime(new QDate(2007, 01, 02), new QTime(16, 05)));
        addMail(model, "Re: Accounts", "Joe Bloggs <joe@bloggs.com>",
                new QDateTime(new QDate(2007, 01, 03), new QTime(14, 18)));
        addMail(model, "Re: Accounts", "Andy <andy@nospam.com>",
                new QDateTime(new QDate(2007, 01, 03), new QTime(14, 26)));
        addMail(model, "Sports", "Linda Smith <linda.smith@nospam.com>",
                new QDateTime(new QDate(2007, 01, 05), new QTime(11, 33)));
        addMail(model, "AW: Sports", "Rolf Newschweinstein <rolfn@nospam.com>",
                new QDateTime(new QDate(2007, 01, 05), new QTime(12, 00)));
        addMail(model, "RE: Sports", "Petra Schmidt <petras@nospam.com>",
                new QDateTime(new QDate(2007, 01, 05), new QTime(12, 01)));

        return model;
    }

private void addMail(QAbstractItemModel model, String subject, String sender, QDateTime date) { model.insertRow(0); model.setData(model.index(0, 0), subject); model.setData(model.index(0, 1), sender); model.setData(model.index(0, 2), date); }
The createMailModel() method is a convenience method provided to simplify the constructor. All it does is to create and return a standard item model describing a collection of emails. Each description is added to the model using addMail(), another convenience method.
    public static void main(String[] args) {

        QApplication.initialize(args);
        CustomFilter filter = new CustomFilter();
        filter.show();
        QApplication.exec();
    }

}
Finally, we provide the main() method to create and show the main application window when the example is run.


Copyright © 2009 Nokia Corporation and/or its subsidiary(-ies) Trademarks
Qt Jambi 4.5.2_01