Custom sortable columns in QFileSystemModel
-
Hi, I am using QTreeView together with QFileSystemModel and my task is to add a new column (Date Created) to the QFileSystemModel. It should also be possible to sort data by this column. For this, I have implemented the code that I provide below.
My question is, have I implemented the solution correctly? Can any problems arise? For example, after "moving" columns?
Here is implementation (I am using Windows 11 with Qt 6.6.2):
#include <QApplication> #include <QTreeView> #include <QFileSystemModel> #include <QSortFilterProxyModel> class CustomSortFilterProxyModel : public QSortFilterProxyModel { public: explicit CustomSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { if (sortColumn() == 4) { QString leftDate = left.data(Qt::DisplayRole).toString(); QString rightDate = right.data(Qt::DisplayRole).toString(); return QDateTime::fromString(leftDate, Qt::ISODate) < QDateTime::fromString(rightDate, Qt::ISODate); } return QSortFilterProxyModel::lessThan(left, right); } }; class Model : public QFileSystemModel { public: void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override { // prevent model from sorting due to setSortingEnabled(true) called first time if (!m_doSort) { m_doSort = true; return; } QFileSystemModel::sort(column, order); } int columnCount(const QModelIndex& parent = QModelIndex()) const override { int cc = QFileSystemModel::columnCount(parent); if (cc == 0) { return 0; } return cc + 1; } QVariant data(const QModelIndex& index,int role) const override { if(index.isValid() && index.column() == 4) { QModelIndex sibling = index.siblingAtColumn(3); switch(role) { case(Qt::DisplayRole): { return fileInfo(sibling).birthTime(); } default: { // return same role as in fourth (Date Modified) column at the current row return QFileSystemModel::data(sibling, role); } } } return QFileSystemModel::data(index, role); } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { if (section == 4) { return QStringLiteral("Date Created"); } } return QFileSystemModel::headerData(section, orientation, role); } private: bool m_doSort = false; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); Model *model = new Model; model->setRootPath(QDir::rootPath()); CustomSortFilterProxyModel* proxyModel = new CustomSortFilterProxyModel; proxyModel->setSourceModel(model); QTreeView *treeView = new QTreeView; treeView->setSortingEnabled(true); treeView->setModel(proxyModel); treeView->setRootIndex(model->index("C:\\Users\\Username\\Desktop\\BOOK")); treeView->setColumnWidth(0, 250); treeView->setWindowTitle("QFileSystemModel Example"); treeView->resize(600, 400); treeView->show(); return app.exec(); }
-
@Jo-Jo
Just a couple of minor observations:-
Your
data()
returns afileInfo(sibling).birthTime()
, which is aQDateTime
. So why does yourlessThan()
convert that to a string and then parse that string back to a datetime in order to compare? That is both slow and error prone? -
Your literal string in
model->index("C:\\Users\\Username\\Desktop\\BOOK")
is a Windows native path. Normally one uses the Qt OS-agnostic representation with/
s instead of\
s. Do otherQFileSystemModel
methods deal with (e.g. return) paths with backslashes or forward slashes? It may not matter, I don't know.
-
-
- You are right, it should be like this:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { if (sortColumn() == 4) { QDateTime leftDate = left.data(Qt::DisplayRole).toDateTime(); QDateTime rightDate = right.data(Qt::DisplayRole).toDateTime(); return leftDate < rightDate; } return QSortFilterProxyModel::lessThan(left, right); }
- I got you, thanks
One problem I have now is when I launch the program, in theory, first there should be only folders, and then files. However, my files and folders are mixed up. How can I fix this problem?
-
@Jo-Jo
Not quite sure exactly what you see or want for the order/visibility of various entries, but are you aware of:-
The default
QFileSystemModel::filter()
includes all entries viaQDir::AllEntries | QDir::NoDotAndDotDot | QDir::AllDirs
. You can change that if e.g. you want to exclude non-directories. -
Or you can control which nodes/leaves are initially expanded/contracted on the
QTreeView
. -
If you want Windows File Explorer-type ordering where directories are sorted and displayed in separate sections even when e.g. sorted by creation time, I'm not seeing anything in
QFileSystemModel
for this(?). For example, Linux default sorting does not treat directories separately from files. So you may have to do this in your sorting functions themselves by looking at the file info duringlessThan()
and including directory versus non-directory as first priority and then creation datetime as second priority?
-
-
@JonB said in Custom sortable columns in QFileSystemModel:
Not quite sure exactly what you see or want for the order/visibility of various entries
Let me make this clear. When I launch the program first time, I want the folders to be shown first, then the files. This is the default behavior when using QTreeView + QFileSystemModel and I want to keep it. However, after using QSortFilterProxyModel this default behavior is broken (please see screenshot).
-
@Jo-Jo
BecauseQSortFilterProxyModel
has no knowledge that you are dealing with file system entries, where you want different sort order for folders versus files. Whereas, presumably, the defaultQFileSystemModel
sorter, whatever that is, does, or does for Windows or whatever. So it seems to me that like I said it is your job to make yourQSortFilterProxyModel::lessThan()
look at the entry types and use whether the items compared are folders or files as the first priority in your comparison code, and only if the two items are the same type (folder/file) then look at the sort column, like creation date or name, as the secondary priority.You can Google for something like
qfilesystemmodel sort
. There are various hits, e.g. https://forum.qt.io/topic/113138/sorting-folders-first-qfilesystemmodel-with-directory-filter, where I have previously referenced a couple of topics. -
@Jo-Jo
That is my understanding, unless you find better by searching.
You might also take heed of https://forum.qt.io/post/569839, where the author states/claims:In order to use QFileSystemModel, you can as well re-implement the "sort" function since the base implementation does nothing:
https://doc.qt.io/qt-5/qfilesystemmodel.html#sort
If you want to sort like the one in the right, I would recommend using the QSortFilterProxyModel as is. If you want something more customizable then re-implementing the QFileSystemModel would be a better option.
While you are looking at that thread, note that it appears Windows File Explorer has further "customized" rules for file name sorting, such as where the file names contains digits where @Christian-Ehrlicher refers to void QCollator::setNumericMode(bool on).
-
@JonB Thank you! Now I am fixed this problem and full implementation currently looks like this (see below) and I would be glad to receive any feedback.
#include <QApplication> #include <QTreeView> #include <QHeaderView> #include <QFileSystemModel> #include <QSortFilterProxyModel> class CustomSortFilterProxyModel : public QSortFilterProxyModel { public: explicit CustomSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { auto model = (QFileSystemModel*)sourceModel(); bool isLeftDir = model->isDir(left); bool isRightDir = model->isDir(right); if ((isLeftDir && isRightDir) || (!isLeftDir && !isRightDir)) { QString const leftName = model->data(left, Qt::DisplayRole).toString(); QString const rightName = model->data(right, Qt::DisplayRole).toString(); const int compare = QString::localeAwareCompare(leftName, rightName); if(compare != 0) { return compare < 0; } } else if (isLeftDir) { return true; } return false; } }; class Model : public QFileSystemModel { public: int columnCount(const QModelIndex& parent = QModelIndex()) const override { int cc = QFileSystemModel::columnCount(parent); if (cc == 0) { return 0; } return cc + 1; } QVariant data(const QModelIndex& index,int role) const override { if(index.isValid() && index.column() == 4) { QModelIndex sibling = index.siblingAtColumn(3); switch(role) { case(Qt::DisplayRole): { return fileInfo(sibling).birthTime(); } default: { // return same role as in fourth (Date Modified) column at the current row return QFileSystemModel::data(sibling, role); } } } return QFileSystemModel::data(index, role); } QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override { if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { if (section == 4) { return QStringLiteral("Date Created"); } } return QFileSystemModel::headerData(section, orientation, role); } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); Model *model = new Model; model->setRootPath(QDir::rootPath()); CustomSortFilterProxyModel* proxyModel = new CustomSortFilterProxyModel; proxyModel->setSourceModel(model); proxyModel->setSortRole(Qt::DisplayRole); QTreeView *treeView = new QTreeView; treeView->setSortingEnabled(true); treeView->setModel(proxyModel); treeView->setRootIndex(model->index("C:\\Users\\Username\\Desktop\\BOOK")); treeView->setColumnWidth(0, 250); treeView->setWindowTitle("QFileSystemModel Example"); treeView->resize(600, 400); treeView->show(); treeView->header()->setSortIndicator(0, Qt::AscendingOrder); return app.exec(); }
-
@Jo-Jo
Well, I could rewrite yourlessThan()
to use fewer lines and smaller expressions, becaue I am like that :) But it's not a big deal.You started out with a
lessThan
which had special code for data comparison in a column. Now you seem to have an implementation which does not look at column data type and just does string comparisons on everything. Is the sufficient? Do you need a more complex "merger" of various approaches depending on data type? Depends what you want. -
@JonB I'm thinking about how to correctly implement comparison taking into account types but have some problems. Currently my code looks like this but not workiing properly. Maybe you can suggest solution?
class CustomSortFilterProxyModel : public QSortFilterProxyModel { QList<int> m_dtCols{3,4}; public: explicit CustomSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { auto model = (QFileSystemModel*)sourceModel(); bool isLeftDir = model->isDir(left); bool isRightDir = model->isDir(right); if ((isLeftDir && isRightDir) || (!isLeftDir && !isRightDir)) { auto leftName = model->data(left, Qt::DisplayRole); auto rightName = model->data(right, Qt::DisplayRole); const int compare = m_dtCols.contains(sortColumn()) ? leftName.toDateTime() < rightName.toDateTime() : QString::localeAwareCompare(leftName.toString(), rightName.toString()); if(compare != 0) { return compare < 0; } } else if (isLeftDir) { return true; } return false; } };
-
@JonB Sorry, sorting by "Date Modified" and "Date Created" columns does not work properly (yes, i use qDebug() for tests)
UPDATE:
I think it should be implemented like thisclass CustomSortFilterProxyModel : public QSortFilterProxyModel { QList<int> m_dtCols{3,4}; public: explicit CustomSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { auto model = (QFileSystemModel*)sourceModel(); bool isLeftDir = model->isDir(left); bool isRightDir = model->isDir(right); if ((isLeftDir && isRightDir) || (!isLeftDir && !isRightDir)) { auto leftName = model->data(left, Qt::DisplayRole); auto rightName = model->data(right, Qt::DisplayRole); if (m_dtCols.contains(sortColumn())) { return (leftName.toDateTime() < rightName.toDateTime()); } const int compare = QString::localeAwareCompare(leftName.toString(), rightName.toString()); if(compare != 0) { return compare < 0; } } else if (isLeftDir) { return true; } return false; } };
But any improvement is appreciated
-
@Jo-Jo
Well, start by checking that goes through thetoDateTime()
path, what their values are and that is getting it right. I don't know what is wrong just by looking at it.Btw, check whether the
QString::localeAwareCompare(0
is case insensitive or case sensitive? Windows, but not Linux, sorts case insensitively, yours might not.You have just updated your code: I'm not sure it is any different from what you first wrote, but if it is and works that's fine.
-
@JonB Everything worked except for the "Date Modified" column. It turned out that Qt returns this date as a QString. I had to convert this string to QDateTime. Now the code looks like this:
class CustomSortFilterProxyModel : public QSortFilterProxyModel { QList<int> m_dtCols{3,4}; public: explicit CustomSortFilterProxyModel(QObject *parent = nullptr) : QSortFilterProxyModel(parent) {} protected: bool lessThan(const QModelIndex &left, const QModelIndex &right) const override { auto model = (QFileSystemModel*)sourceModel(); bool isLeftDir = model->isDir(left); bool isRightDir = model->isDir(right); if ((isLeftDir && isRightDir) || (!isLeftDir && !isRightDir)) { auto leftName = model->data(left, Qt::DisplayRole); auto rightName = model->data(right, Qt::DisplayRole); //return (leftName.toDateTime() < rightName.toDateTime()); // OK, works fine for dates if (m_dtCols.contains(sortColumn())) { if (sortColumn() == 3) { QDateTime dt1 = QDateTime::fromString(leftName.toString(), "M/d/yyyy h:mm AP"); QDateTime dt2 = QDateTime::fromString(rightName.toString(), "M/d/yyyy h:mm AP"); return dt1 < dt2; } return (leftName.toDateTime() < rightName.toDateTime()); } const int compare = QString::localeAwareCompare(leftName.toString(), rightName.toString()); if(compare != 0) { return compare < 0; } } else if (isLeftDir) { return true; } return false; } };
However, I'm not sure that I'm setting the date format correctly. It can change depending on the OS settings, or am I wrong?
-
@Jo-Jo said in Custom sortable columns in QFileSystemModel:
I had to convert this string to QDateTime
You already converted the QVariant to a QDateTime above - so why do you use the way through QString now again?
-
@Christian-Ehrlicher said in Custom sortable columns in QFileSystemModel:
You already converted the QVariant to a QDateTime above - so why do you use the way through QString now again?
Because "Date Modified" column is created by Qt and Qt using QString for this column. If I convert QVariant to QDateTime, in this case comparison does not work properly
-
Then you should debug why ... e.g. with a qDebug() statement as @JonB already wrote some time ago....
-
@Jo-Jo said in Custom sortable columns in QFileSystemModel:
Everything worked except for the "Date Modified" column. It turned out that Qt returns this date as a QString.
I am surprised at this, but untested. You are now talking about a "Date Modified" column inbuilt into
QFileSystemModel
, and not the "creation date" column you added for yourbirthTime()
? But that is QDateTime QFileSystemModel::lastModified(). So I don't understand where you are getting any string for a datetime which you need to convert. -
@JonB said in Custom sortable columns in QFileSystemModel:
You are now talking about a "Date Modified" column inbuilt into QFileSystemModel, and not the "creation date" column you added for your birthTime()?
Yes. This column value is QString, not QDateTime. You may check sources: https://codebrowser.dev/qt5/qtbase/src/widgets/dialogs/qfilesystemmodel.cpp.html#808