Walletfox.com

Sorting and filtering table data with QSortFilterProxyModel


This article shows how to sort and filter table data with QSortFilterProxyModel. The basis for sorting/filtering is a subclass of QAbstractTableModel which contains data about planets in the solar system. The data of the source model can be sorted (in ascending or descending manner) by clicking on the header of a column. Moreover, the data can be filtered simultaneously by gravity and density.

Implementation details, MainWindow

The planetary data (i.e. the source model) is represented by PlanetTableModel which inherits from QAbstractTableModel. Sorting and filtering is achieved with the help of ProxyModel which inherits from QSortFilterProxyModel. The sorted/filtered data is then displayed in QTableView. The user can filter the data with the help of filterGravitySpinBox and filterDensitySpinBox.

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);

private:
    QTableView* view;
    PlanetTableModel* model;
    ProxyModel* proxyModel;

    QLabel* filterGravityLabel;
    QLabel* filterDensityLabel;
    QDoubleSpinBox* filterGravitySpinBox;
    QDoubleSpinBox* filterDensitySpinBox;

    void createWidgets();
    void createConnections();
    void createLayout();
};

In the MainWindow constructor we instantiate and populate the table model. The model is populated with the help of a QList passed to the constructor of the source model. We also instantiate the proxy model and associate it with the source model with the help of the method setSourceModel(). We also create an instance of QTableView and associate it with the proxyModel. The only thing that needs to be done to enable sorting by clicking on the view's horizontal header, is to call view->setSortingEnabled(true). Repeated clicking on the header enables to alternate between ascending and descending order.

MainWindow::MainWindow(QWidget* parent):
    QMainWindow(parent)
{
    QList<Planet> planetList;
    planetList.append(Planet("Jupiter", 23.1, 1.326));
    planetList.append(Planet("Saturn", 9.0, 0.687));
    planetList.append(Planet("Uranus", 	8.7, 1.271));
    planetList.append(Planet("Neptune", 11.0, 1.638));
    planetList.append(Planet("Earth", 9.8, 5.514));
    planetList.append(Planet("Venus", 8.9, 5.243));
    planetList.append(Planet("Mars", 3.7, 3.933));
    planetList.append(Planet("Mercury", 3.7, 5.427));

    model = new PlanetTableModel(planetList);
    proxyModel = new ProxyModel;
    proxyModel->setSourceModel(model);

    view = new QTableView;
    view->setModel(proxyModel);
    view->verticalHeader()->setMinimumWidth(25);
    view->verticalHeader()->setDefaultAlignment(Qt::AlignCenter);
    view->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
    view->setSortingEnabled(true);
    view->sortByColumn(0, Qt::AscendingOrder);

    createWidgets();
    createLayout();
    createConnections();

    setWindowTitle("Sorting and filtering table data");
    resize(370,370);

}

Warning: The name of the method QHeaderView::setResizeMode(...) changed in Qt5

Qt 4
view->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
Qt 5
view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);

In createWidgets() we create labels and spinboxes that enable to set the minimum gravity and density. We set the gravity step to 2.5 m/s2 and density step to 0.5 g/cm3

void MainWindow::createWidgets(){
    filterGravityLabel = new QLabel("Gravity more than:");
    filterGravitySpinBox = new QDoubleSpinBox;
    filterGravitySpinBox->setValue(0.0);
    filterGravitySpinBox->setSingleStep(2.5);

    filterDensityLabel = new QLabel("Density more than:");
    filterDensitySpinBox = new QDoubleSpinBox;
    filterDensitySpinBox->setValue(0.0);
    filterDensitySpinBox->setSingleStep(0.5);
}

In createConnections() we connect the signals valueChanged(double) of the filterGravitySpinBox and filterDensitySpinBox, to the slots that update the minimum values for gravity and density in the proxyModel.

void MainWindow::createConnections(){
    QObject::connect(filterGravitySpinBox, SIGNAL(valueChanged(double)),
                     proxyModel, SLOT(setMinGravity(double)));
    QObject::connect(filterDensitySpinBox, SIGNAL(valueChanged(double)),
                     proxyModel, SLOT(setMinDensity(double)));
}

Source model

The source model for planets is represented by a subclass of QAbstractTableModel with user-defined rowCount(), columnCount(), data() and headerData(). The planetary data is stored in the list QList<Planet> m_planetList which is populated in the constructor of the source model.

class PlanetTableModel : public QAbstractTableModel
{
public:
    PlanetTableModel(QObject* parent = 0);
    PlanetTableModel(const QList<Planet>& planetList,
                         QObject *parent = 0);
    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    int columnCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data (const QModelIndex & index,
                   int role = Qt::DisplayRole) const;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role = Qt::DisplayRole) const;
private:
    QList<Planet> m_planetList;
};

The individual data of a Planet is represented by a struct Planet which contains three members, namely m_name, m_gravity and m_density.

struct Planet{
    Planet(const QString& name = QString(), double gravity = 0.0,
           double density = 0.0):
        m_name(name), m_gravity(gravity), m_density(density){
    }

    QString m_name;
    double m_gravity;
    double m_density;
};
Note: This is a relatively simple situation, thus the Planet is represented by a struct. More complex data might require validation. In such situations, a class with get and set methods might be more suitable.

In the constructor we populate the table model with data. The method rowCount() returns the size of the m_planetList, i.e. the number of planets, columnCount() returns 3 (the name, gravity and density).

PlanetTableModel::PlanetTableModel(const QList<Planet> &planetList,
                                           QObject* parent):
    QAbstractTableModel(parent)
{
    this->m_planetList = planetList;
}

int PlanetTableModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return 3;
}

int PlanetTableModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return m_planetList.size();
}

The method data() decides how the data is presented in the table. In the method, we first check the validity and correct range of an index. If the index is valid, we create a reference to a planet for a particular index row and display its name in column 0, gravity in column 1 and density in column 2. The text of the 1st and 2nd column is aligned to the middle in both directions, the text of the 0th column is aligned to the left horizontally and to the middle vertically.

QVariant PlanetTableModel::data (const QModelIndex & index,
               int role) const {
    if (!index.isValid())
        return QVariant();
    if (index.row() >= m_planetList.size() || index.row() < 0)
        return QVariant();

    if (role == Qt::DisplayRole) {
        const Planet& planet = m_planetList.at(index.row());
        switch (index.column()) {
        case 0:
            return planet.m_name;
        case 1:
            return planet.m_gravity;
        case 2:
            return planet.m_density;
        default:
            return QVariant();
        }
    }
    else if (role == Qt::TextAlignmentRole){
        if (index.column() == 0)
            return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
        else
            return Qt::AlignCenter;
    }
    return QVariant();
}

The method headerData() determines how the data is presented in the headers. The horizontal header displays the text "Planet", "Gravity [m/s^2]" and "Density [g/cm^3]" in the 0th, 1st and 2nd columns, respectively. Since we want the vertical header to return row numbers 1,2,3...8 (rather than 0,1,2...7) we return section + 1.

QVariant PlanetTableModel::headerData(int section,
                                          Qt::Orientation orientation,
                    int role) const {
    if (role != Qt::DisplayRole)
        return QVariant();

    if (orientation == Qt::Horizontal) {
        switch (section) {
        case 0:
            return tr("Planet");
        case 1:
            return tr("Gravity [m/s^2]");
        case 2:
            return tr("Density [g/cm^3]");
        default:
            return QVariant();
        }
    }
    return section + 1;
}

Custom filtering model

Below you can see the implementation of a subclass of QSortFilterProxyModel responsible for filtering. The custom class ProxyModel contains two member variables m_minGravity and m_minDensity which hold the limit values for gravity and density entered by the user via setMinGravity(double) and setMinDensity(double). The filtering is implemented in the method filterAcceptsRow() method. The implementation details follow.

class ProxyModel : public QSortFilterProxyModel
{
    Q_OBJECT
public:
    ProxyModel(QObject* parent = 0);
    bool filterAcceptsRow(int source_row,
                          const QModelIndex &source_parent) const;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role) const;

public slots:
    void setMinGravity(double minGravity);
    void setMinDensity(double minDensity);
private:
    double m_minGravity;
    double m_minDensity;
};

In the constructor of the ProxyModel we initialize the member variables m_minGravity and m_minDensity. The slots setMinGravity() and setMinDensity() get called anytime the user changes the value for minimum density or minimum gravity in the widgets. Notice, that the set methods also call invalidateFilter() which invalidates the current filtering.

ProxyModel::ProxyModel(QObject *parent): QSortFilterProxyModel(parent),
    m_minGravity(0.0), m_minDensity(0.0)
{
}

void ProxyModel::setMinGravity(double minGravity){
    if(m_minGravity != minGravity)
        m_minGravity = minGravity;
    invalidateFilter();
}

void ProxyModel::setMinDensity(double minDensity){
    if(m_minDensity != minDensity)
        m_minDensity = minDensity;
    invalidateFilter();
}

The method filterAcceptsRow() returns true if the planet corresponding to the source_row and source_parent should be included in the model. Our aim is to include all the planets that satisfy both the minimum density and minimum gravity criterion. QModelIndex indG refers to the gravity of the planet, while indD refers to its density. If either the planet's gravity or the planet's density does not satisfy the limit values entered by the user, the planet won't show up in the table, i.e. the method will return false.

bool ProxyModel::filterAcceptsRow(int source_row,
                                  const QModelIndex &source_parent) const{

    QModelIndex indG = sourceModel()->index(source_row,
                                               1, source_parent);
    QModelIndex indD = sourceModel()->index(source_row,
                                               2, source_parent);
    if(sourceModel()->data(indG).toDouble() < m_minGravity ||
            sourceModel()->data(indD).toDouble() < m_minDensity)
        return false;
    return true;
}

Finally, we also override headerData() (for sorting rather than filtering purposes). Our aim is to preserve the numbering in the vertical header in form 1,2.....8, irrespective of sorting. For this reason, we return the header data of the source model. If we did not do this, anytime we sorted the data, the numbering of the rows would change to e.g. 7, 8, 3, 6...1.

QVariant ProxyModel::headerData(int section, Qt::Orientation orientation,
                                int role) const {
    return sourceModel()->headerData(section, orientation,
                                     role);
}

That's it! Further details of the implementation can be found in the source files at the beginning of this article.

Tagged: Qt