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 subclassedQFileSystemModel
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 usingQSortFilterProxyModel
andfilterAcceptsRow
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 afaikQFileSystemModel
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:-
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 usesetRootPath
? -
Subclass
QAbstractItemModel
.
This solution is more or less clear, however, it's missing some of the important things that go withQFileSystemModel
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 withQFileIconProvider
.
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.
-
-
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. -
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:- Where can I find them?
- Is it really a better path than implementing QAbstractItemModel?
-
@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:- Where can I find them?
- Is it really a better path than implementing QAbstractItemModel?
-
@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 aQTreeView()
similar to the way thatQFileSystemModel()
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:
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)
-
@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 aQTreeView()
similar to the way thatQFileSystemModel()
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:
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 usingQSortFilterProxyModel
for that? -
@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 usingQSortFilterProxyModel
for that?@midnightdim Yes,
QSortFilterProxyModel
is the right way to go. -
@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.
-