Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Display custom list of files in QTreeView using QFileSystemModel or QAbstractItemModel



  • I'm building an open source PySide6 app based on the custom file browser in QTreeView.
    I've already subclassed QFileSystemModel to display a custom column with some extra data.

    Now my goal is to display a specific subset of files (they can be located on different drives) in a treeview.

    To simplify things imagine that I have a function:

    def files_to_display():
        return ['C:\file1', 'D:\file2', 'D:\folder1\file3']
    

    Now I need to display these files in my QTreeView. I tried using QSortFilterProxyModel and filterAcceptsRow to filter out everything else and it worked. However on a relatively large number of files it's extremely slow and unusable. I'm pretty sure a simpler custom file tree would work faster because afaik QFileSystemModel tracks the folder state and runs other extra stuff that I can live without.

    I'm not sure how to solve this problem.
    I see basically 2 ways:

    1. Somehow cut out what I don't need from QFileSystemModel.
      With this solution I don't fully understand how I do this. In particular, how do I fill the model with the data from my function? How do it use setRootPath?

    2. Subclass QAbstractItemModel.
      This solution is more or less clear, however, it's missing some of the important things that go with QFileSystemModel out of the box: I need the columns and the data it provides (name, size, type, modification date), I also need file/folder icons that I'm using with QFileIconProvider.

    So basically I'd like to use a light-weighted version of QFileSystemModel without watching the file system and with my list of files.

    I'm open to alternative solutions.


  • Lifetime Qt Champion

    Hi,

    You should take a look at the sources of QFileSystemModel to see how things are implemented there. You can then see what is the most useful to you.

    Otherwise, you can also leverage Python's os.walk to parse your folders of interest.



  • @SGaist Thanks, but my goal is not to parse folders of interest. As I mentioned in my post, I have a list of specific files that I want to display, so os.walk is not relevant.
    As for the sources of QFileSystemModel:

    1. Where can I find them?
    2. Is it really a better path than implementing QAbstractItemModel?

  • Lifetime Qt Champion



  • @jsulm Thanks. I will do my best to study this, but my initial request is still actual. My knowledge and experience are quite limited, I'm not a professional programmer, so I appreciate any help and advice here.



  • @midnightdim What follows is a (pretty lengthy/complex) example of what you might be wanting. It's adapted from a project of mine that sounds similar to your own. FileSystemModelLite() is just about enough to represent a predefined list of files in a QTreeView() similar to the way that QFileSystemModel() does, but you don't get any bells or whistles and, as this is a static model, it will not respond to updates from the file system and is not editable. Also, it expects the files provided to exist, i.e. it does no checking. Final caveat, I believe this should work fine on Windows (your list of example files indicated you were), but, as I don't have a Windows machine to test it on, I can't be 100% sure.!

    If it all works, it should look something like the following:

    screenshot

    Here's the code:

    import os.path as osp
    import posixpath, mimetypes
    import time
    from typing import Any, List, Union
    
    from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
    from PyQt5.QtWidgets import QWidget, QVBoxLayout, QTreeView, QFileIconProvider
    
    FSMItemOrNone = Union["_FileSystemModelLiteItem", None]
    
    
    def sizeof_fmt(num, suffix="B"):
        """Creates a human readable string from a file size"""
        for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
            if abs(num) < 1024.0:
                return f"{num:3.1f}{unit}{suffix}"
            num /= 1024.0
        return f"{num:.1f}Yi{suffix}"
    
    
    class _FileSystemModelLiteItem(object):
        """Represents a single node (drive, folder or file) in the tree"""
    
        def __init__(
            self,
            data: List[Any],
            icon=QFileIconProvider.Computer,
            parent: FSMItemOrNone = None,
        ):
            self._data: List[Any] = data
            self._icon = icon
            self._parent: _FileSystemModelLiteItem = parent
            self.child_items: List[_FileSystemModelLiteItem] = []
    
        def append_child(self, child: "_FileSystemModelLiteItem"):
            self.child_items.append(child)
    
        def child(self, row: int) -> FSMItemOrNone:
            try:
                return self.child_items[row]
            except IndexError:
                return None
    
        def child_count(self) -> int:
            return len(self.child_items)
    
        def column_count(self) -> int:
            return len(self._data)
    
        def data(self, column: int) -> Any:
            try:
                return self._data[column]
            except IndexError:
                return None
    
        def icon(self):
            return self._icon
    
        def row(self) -> int:
            if self._parent:
                return self._parent.child_items.index(self)
            return 0
    
        def parent_item(self) -> FSMItemOrNone:
            return self._parent
    
    
    class FileSystemModelLite(QAbstractItemModel):
        def __init__(self, file_list: List[str], parent=None, **kwargs):
            super().__init__(parent, **kwargs)
    
            self._icon_provider = QFileIconProvider()
    
            self._root_item = _FileSystemModelLiteItem(
                ["Name", "Size", "Type", "Modification Date"]
            )
            self._setup_model_data(file_list, self._root_item)
    
        def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
            if not index.isValid():
                return None
    
            item: _FileSystemModelLiteItem = index.internalPointer()
            if role == Qt.DisplayRole:
                return item.data(index.column())
            elif index.column() == 0 and role == Qt.DecorationRole:
                return self._icon_provider.icon(item.icon())
            return None
    
        def flags(self, index: QModelIndex) -> Qt.ItemFlags:
            if not index.isValid():
                return Qt.NoItemFlags
            return super().flags(index)
    
        def headerData(
            self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
        ) -> Any:
            if orientation == Qt.Horizontal and role == Qt.DisplayRole:
                return self._root_item.data(section)
            return None
    
        def index(
            self, row: int, column: int, parent: QModelIndex = QModelIndex()
        ) -> QModelIndex:
            if not self.hasIndex(row, column, parent):
                return QModelIndex()
    
            if not parent.isValid():
                parent_item = self._root_item
            else:
                parent_item = parent.internalPointer()
    
            child_item = parent_item.child(row)
            if child_item:
                return self.createIndex(row, column, child_item)
            return QModelIndex()
    
        def parent(self, index: QModelIndex) -> QModelIndex:
            if not index.isValid():
                return QModelIndex()
    
            child_item: _FileSystemModelLiteItem = index.internalPointer()
            parent_item: FSMItemOrNone = child_item.parent_item()
    
            if parent_item == self._root_item:
                return QModelIndex()
    
            return self.createIndex(parent_item.row(), 0, parent_item)
    
        def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
            if parent.column() > 0:
                return 0
    
            if not parent.isValid():
                parent_item = self._root_item
            else:
                parent_item = parent.internalPointer()
    
            return parent_item.child_count()
    
        def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
            if parent.isValid():
                return parent.internalPointer().column_count()
            return self._root_item.column_count()
    
        def _setup_model_data(
            self, file_list: List[str], parent: "_FileSystemModelLiteItem"
        ):
            def _add_to_tree(_file_record, _parent: "_FileSystemModelLiteItem", root=False):
                item_name = _file_record["bits"].pop(0)
                for child in _parent.child_items:
                    if item_name == child.data(0):
                        item = child
                        break
                else:
                    data = [item_name, "", "", ""]
                    if root:
                        icon = QFileIconProvider.Computer
                    elif len(_file_record["bits"]) == 0:
                        icon = QFileIconProvider.File
                        data = [
                            item_name,
                            _file_record["size"],
                            _file_record["type"],
                            _file_record["modified_at"],
                        ]
                    else:
                        icon = QFileIconProvider.Folder
    
                    item = _FileSystemModelLiteItem(data, icon=icon, parent=_parent)
                    _parent.append_child(item)
    
                if len(_file_record["bits"]):
                    _add_to_tree(_file_record, item)
    
            for file in file_list:
                file_record = {
                    "size": sizeof_fmt(osp.getsize(file)),
                    "modified_at": time.strftime(
                        "%a, %d %b %Y %H:%M:%S %Z", time.localtime(osp.getmtime(file))
                    ),
                    "type": mimetypes.guess_type(file)[0],
                }
    
                drive = True
                if "\\" in file:
                    file = posixpath.join(*file.split("\\"))
                bits = file.split("/")
                if len(bits) > 1 and bits[0] == "":
                    bits[0] = "/"
                    drive = False
    
                file_record["bits"] = bits
                _add_to_tree(file_record, parent, drive)
    
    
    class Widget(QWidget):
        def __init__(self, parent=None, **kwargs):
            super().__init__(parent, **kwargs)
    
            file_list = [
                "/var/log/boot.log",
                "/var/lib/mosquitto/mosquitto.db",
                "/tmp/some.pdf",
            ]
            self._fileSystemModel = FileSystemModelLite(file_list, self)
    
            layout = QVBoxLayout(self)
    
            self._treeView = QTreeView(self)
            self._treeView.setModel(self._fileSystemModel)
            layout.addWidget(self._treeView)
    
    
    if __name__ == "__main__":
        from sys import argv, exit
        from PyQt5.QtWidgets import QApplication
    
        a = QApplication(argv)
        w = Widget()
        w.show()
        exit(a.exec())
    

    This was tested using Python 3.8.5 and PyQt5.15.3 (and PySide2 5.15.2) on Ubuntu 20.04.2LTS.

    Hope this helps :o)



  • @jazzycamel Thank you so much! This works for me and looks very close to what I need.
    Quick question: I'll probably need to add sorting to this view/model, would you recommend using QSortFilterProxyModel for that?



  • @midnightdim Yes, QSortFilterProxyModel is the right way to go.



  • @jazzycamel
    Dear camel,

    Now that this topic is resolved....

    In your signature you have:

    3. I know how super() works

    I well-remember when and why you appended this over a year ago, because of a "difficult" Python/PyQt5 user we had on this site at the time! (I had had similar run-ins with that user.) Purely FYI, that user got "removed", so you could risk removing this from your sig now ;-)

    All the Best.


Log in to reply