Subclassing QFileSystemModel to filter images by size

This article shows how to subclass QFileSystemModel to enable filtering of image files by their size. The application allows to enter lower limit for file size into a spinbox and highlights all the files that are larger than the lower limit, as well as the folders that contain those files. This can be seen below.

The present implementation addresses two points associated with filtering QFileSystemModel:

1. QFileSystemModel is lazily populated, thus the number of child directories and/or files is normally not known unless the user expands the parent folder. The implementation presented in this article uses an alternative technique for retrieving information about the contents of a directory, namely QDirIterator with the flag QDirIterator::Subdirectories. This approach enables to avoid forced population of the QFileSystemModel.

2. This implementation chooses to subclass QFileSystemModel rather than to use QSortFilterProxyModel. Although QSortFilterProxyModel is the most common approach for filtering custom data models, when using QSortFilterProxyModel in conjuction with QFileSystemModel, the view resets to an invalid index as the root index whenever a filter such as setFilterRegExp() is applied. This requires some additional code handling, making the implementation less transparent. For this reason, subclassing QFileSystemModel appears to be more straightforward.

Implementation of MainWindow

Below you can see the declaration of the MainWindow holding a pointer to an instance of QTreeView, as well as to a custom MyFileSystemModel instance (more on this later). Notice also the QSpinBox* sizeLimitSpinBox where we enter the size limit for our image files.

class MainWindow : public QMainWindow
{
    Q_OBJECT
    
public:
    explicit MainWindow(QWidget *parent = 0);

private:
    QTreeView *view;
    MyFileSystemModel *model;

    QLabel* sizeLimitLabel;
    QSpinBox* sizeLimitSpinBox;
};

In the MainWindow constructor we initialize the model, the tree and the widgets. Notice, that we apply setNameFilters() to our model to filter out certain types of images. The method setNameFilters() is part of the standard implementation of QFileSystemModel. To allow automatic resizing of the first column whenever we expand the file tree, we set the resize mode of the tree header to ResizeToContents.

Notice the connection between the signal valueChanged(int) of the sizeLimitSpinBox and the slot of our custom file system model identifyPathsOfSize(int). This connection enables to update the model anytime we change the lower limit in the spinbox.

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    QString path = QDir::currentPath() + "/mydocs";

    model = new MyFileSystemModel(this);
    model->setRootPath(path);
    model->setNameFilters(QStringList()<< "*.png" << "*.jpg");

    view = new QTreeView(this);
    view->setModel(model);
    view->setRootIndex(model->index(path));
    view->setAlternatingRowColors(true);
    view->header()->setResizeMode(0, QHeaderView::ResizeToContents);
    view->header()->setStretchLastSection(true);

    sizeLimitLabel = new QLabel("Images larger than [KB]");
    sizeLimitSpinBox = new QSpinBox;
    sizeLimitSpinBox->setMaximum(2000);
    sizeLimitSpinBox->setSingleStep(10);

    QGridLayout *mainLayout = new QGridLayout;
    mainLayout->addWidget(view,0,0,1,5);
    mainLayout->addWidget(sizeLimitLabel,1,3,1,1);
    mainLayout->addWidget(sizeLimitSpinBox,1,4,1,1);

    QWidget* widget = new QWidget(this);
    widget->setLayout(mainLayout);
    setCentralWidget(widget);

    setWindowTitle(tr("File system model with a size filter"));
    resize(480, 330);

    QObject::connect(sizeLimitSpinBox, SIGNAL(valueChanged(int)),
                     model, SLOT(identifyPathsOfSize(int)));
}

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

Qt 4
view->header()->setResizeMode(0, QHeaderView::ResizeToContents);
Qt 5
view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);

Subclassing QFileSystemModel

To implement the desired functionality, we subclass the QFileSystemModel. You can see the header below. The slot identifyPathsOfSize(int sizeLimit) is invoked anytime we change the lower limit in the spinbox. The role of identifyPathsOfSize(int) is to identify all the paths leading to the images that fit the prescribed size filter. In case the filtering method is invoked for the first time, we also populate the dictionary m_fileToSizeHash with all the files in the directory tree and their respective sizes. After that, we identify all the file paths from m_fileToSizeHash that fit the prescribed size filter and append them to m_filteredSet. The method data() is responsible for rendering all the paths stored in m_filteredSet in red color.

class MyFileSystemModel : public QFileSystemModel
{
    Q_OBJECT
public:
    MyFileSystemModel(QObject* parent = 0);
    QVariant data(const QModelIndex& index,
               int role = Qt::DisplayRole) const;

public slots:
    void identifyPathsOfSize(int sizeLimit);

private:
    QHash<QString, qint64> m_fileToSizeHash;
    QSet<QString> m_filteredSet;

    void populateFileToSizeHash();
    void populateFilteredSet(int sizeLimit);
    void addAllParentalPaths(const QString& filePath);
};

Note: QSet has been chosen to represent the collection of filtered paths. This is because QSet is a hash-table based container and as such provides a better performance for containment testing (whether a collection contains an element or not). QSet has potentially a constant time O(1) order of complexity for containment testing, while QList would have a linear time O(n) order of complexity.

We decided to use QHash rather than QMap because our example does not require a collection sorted by keys (QMap).

Below you can see the body of the slot identifyPathsOfSize(int). We firstly check if the hash m_fileToSizeHash has been already populated with all the files and their sizes. If not, we call the method populateFileToSizeHash() and construct a QDirIterator instance with rootDirectory() as an argument. Notice, that rootDirectory() has already been prefiltered before entering identifyPathsOfSize(), i.e. in our case it contains only png and jpg files.

void MyFileSystemModel::identifyPathsOfSize(int sizeLimit){
    if(m_fileToSizeHash.isEmpty())
        populateFileToSizeHash();
    populateFilteredSet(sizeLimit);
}

void MyFileSystemModel::populateFileToSizeHash(){
    QDirIterator it(rootDirectory(), QDirIterator::Subdirectories);
    while (it.hasNext()) {
        it.next();
        const QFileInfo& fInfo = it.fileInfo();
        if(fInfo.isFile())
            m_fileToSizeHash[it.filePath()] = fInfo.size();
    }
}
In our case, the resulting m_fileToSizeHash looks like this:

"D:/filesystemexample/mydocs/europe/zurich/pic1.jpg", 22867
"D:/filesystemexample/mydocs/asia/india/ladakh/pic1.png", 61384
"D:/filesystemexample/mydocs/asia/india/pic2.png", 56996
"D:/filesystemexample/mydocs/europe/budapest/pic2.jpg", 17282
"D:/filesystemexample/mydocs/europe/zurich/pic2.jpg", 24854
"D:/filesystemexample/mydocs/europe/budapest/pic1.jpg", 17515
Note: For large directories, you might consider to place the method populateFileToSizeHash() into a separate thread.

Provided the m_fileToSizeHash is populated, we call the method populateFilteredSet(int). We empty the list containing the paths from any prior size filters and perform size filtering. At the end, we emit dataChanged(QModelIndex(),QModelIndex()) for the change to be reflected in the view.

void MyFileSystemModel::populateFilteredSet(int sizeLimit){
    m_filteredSet.clear();
    qint64 sizeLimitInBytes = sizeLimit*1024;

    QHashIterator i(m_fileToSizeHash);
    while (i.hasNext()) {
        i.next();
        if(i.value() > sizeLimitInBytes)
            addAllParentalPaths(i.key());
    }
    emit dataChanged(QModelIndex(),QModelIndex());
}

The method addAllParentalPaths(QString), appends paths containing images larger than the lower limit to the m_filteredSet. The code appends the file path of the image, as well as the entire branch of parental paths leading to the image. This is ensured by a combination of QString::lastIndexOf("/") and QString::truncate(int) methods.

void MyFileSystemModel::addAllParentalPaths(const QString &filePath){
    QString pathToAdd = filePath;
    while(pathToAdd != rootPath()){
        if(m_filteredSet.contains(pathToAdd))
            break;
        else {
            m_filteredSet.insert(pathToAdd);
            int lastInd = pathToAdd.lastIndexOf("/");
            pathToAdd.truncate(lastInd);
        }
    }
}

For one of the images in our example folder, the while block produces the following series of paths:

"D:/filesystemexample/mydocs/asia/india/ladakh/pic1.png"
"D:/filesystemexample/mydocs/asia/india/ladakh"
"D:/filesystemexample/mydocs/asia/india"
"D:/filesystemexample/mydocs/asia"
Note: The file sizes provided by QFileInfo are reported in bytes, i.e. the limit values entered by the user in KB (kibibytes) have to be converted to bytes, i.e. multiplied by 1024. 1 kibibyte (1 KB) has 1024 bytes, while 1 kilobyte (1 kB) has 1000 bytes.
Note: Qt uses "/" as a universal directory separator, so in this case, there is no need for system-specific adjustments of the file paths.

In our concrete example, the initial list of paths contained in rootDirectory() looks like the one below. After applying a size filter of 30 KB, the resulting m_filteredSet list contains only the paths in bold:

"D:/filesystemexample/mydocs/asia"
"D:/filesystemexample/mydocs/asia/india"
"D:/filesystemexample/mydocs/asia/india/ladakh"
"D:/filesystemexample/mydocs/asia/india/ladakh/pic1.png"
"D:/filesystemexample/mydocs/asia/india/pic2.png"
"D:/filesystemexample/mydocs/europe"
"D:/filesystemexample/mydocs/europe/budapest"
"D:/filesystemexample/mydocs/europe/budapest/pic1.jpg"
"D:/filesystemexample/mydocs/europe/budapest/pic2.jpg"
"D:/filesystemexample/mydocs/europe/zurich"
"D:/filesystemexample/mydocs/europe/zurich/pic1.jpg"
"D:/filesystemexample/mydocs/europe/zurich/pic2.jpg"
"D:/filesystemexample/mydocs/north_america"

Last, in order to highlight the files and the parents of the files larger than the lower limit, we need to override the method data(const QModelIndex &index, int role). Whenever the path of the index matches one of the paths contained in m_filteredSet, we render its text in red color.

QVariant MyFileSystemModel::data(const QModelIndex &index,
                                 int role) const {
    if(role == Qt::TextColorRole){
        if(m_pathsLargerThan.contains(filePath(index)))
            return QColor(Qt::red);
    }
    return QFileSystemModel::data(index, role);
}
That's it! Try out the example with the folder provided in the sources or with your own folder.

© Walletfox.com, 2017