Issue
I have a Treeview
(inherits from QTreeView
) with model QFileSystemModel
. This Treeview
object is added to another widget with a QPushButton
. The button will open a directory through a QFileDialog
, and call the Treeview
's set_directory
method. The method will set the root path of the model to the selected directory and will modify the root index of the tree view. The top-most item in the treeview is then selected.
However, when selecting a directory for the first time, the top-most item is not selected. Commenting out line 11: self.model.directoryLoaded.connect(self.set_directory)
, solves the issue. But this means the set_directory
method is called twice:
# default directory opened
<class 'NoneType'>
# open new directory, set_directory called twice
<class 'NoneType'>
<class 'PyQt5.QtWidgets.QFileSystemModel'>
As seen on the command line output, the QModelIndex.model()
method returns a NoneType when selecting a directory for the first time. How do I set the treeview's current index to the top most item without calling the set_directory
method twice? And why is the QModelIndex
model a NoneType when the directory has not been visited?
Main script below:
from PyQt5.QtWidgets import QPushButton, QTreeView, QFileSystemModel, QWidget, QFileDialog, QApplication, QHBoxLayout
import sys
class Treeview(QTreeView):
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(".")
# self.model.directoryLoaded.connect(self.set_directory)
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setCurrentIndex(self.model.index(0, 0, self.rootIndex()))
print(type(self.model.index(0, 0, self.rootIndex()).model()))
class MainWidget(QWidget):
def __init__(self, parent=None):
super(QWidget, self).__init__(parent)
self.tview = Treeview(self)
vlayout = QHBoxLayout(self)
vlayout.addWidget(self.tview)
open_button = QPushButton(self)
open_button.clicked.connect(self.open_dir)
vlayout.addWidget(open_button)
def open_dir(self):
filepath = QFileDialog.getExistingDirectory(self)
if filepath:
self.tview.set_directory(filepath)
if __name__ == '__main__':
app = QApplication(sys.argv)
widget = MainWidget()
widget.show()
sys.exit(app.exec_())
Solution
There are three problems:
directoryLoaded
is called whenever the contents of a directory are loaded the first time, which happens asynchronously on a separate thread; if the contents of the "root" directory has never been loaded yet, it will have no children yet, so any attempt to access the children of the root index will return invalid indexes (that's why you getNone
);- the signal is also sent whenever a directory is expanded in the tree for the first time, which creates a problem if you connect it to
set_directory
: if you open the folder "home" and then expand the directory "documents", the model will load all its contents and emit thedirectoryLoaded
for the "documents" folder, which will then callset_directory
in turn once again; - the file system model also sorts items (alphabetically by default, with directories first), and this means that it will need some more time to get what you believe is the first item: for performance reasons, the OS returns the contents of a directory depending on the file system, which depends on its implementation: sometimes it's sorted by creation/modification time, others by the physical position/index of the blocks, etc;
Considering the above, you cannot rely on directoryLoaded
(and surely you should not connect it to set_directory
), but you can use the layoutChanged
signal, since it's always emitted whenever sorting has completed (and QFileSystemModel always sorts the model when the root changes); the only catch is that you must do it only when needed.
The solution is to create a function that tries to set the top item, if it's not valid then it will connect itself to the layoutChanged
signal; at that point, the signal will be emitted when the model has completed its job, and the top index has become available. Using a flag helps us to know if the signal has been connected, and then disconnect it, which is important in case you need to support sorting.
class Treeview(QTreeView):
layoutCheck = False
def __init__(self, parent=None):
super(QTreeView, self).__init__(parent)
self.model = QFileSystemModel()
self.setModel(self.model)
self.set_directory(QDir.currentPath())
self.setSortingEnabled(True)
def setTopIndex(self):
topIndex = self.model.index(0, 0, self.rootIndex())
print(topIndex.isValid(), topIndex.model())
if topIndex.isValid():
self.setCurrentIndex(topIndex)
if self.layoutCheck:
self.model.layoutChanged.disconnect(self.setTopIndex)
self.layoutCheck = False
else:
if not self.layoutCheck:
self.model.layoutChanged.connect(self.setTopIndex)
self.layoutCheck = True
def set_directory(self, path):
self.setRootIndex(self.model.setRootPath(path))
self.setTopIndex()
Please consider that the layoutCheck
flag is very important, for many reasons:
- as explained before,
layoutChanged
is always emitted when the model is sorted; this not only happens when trying to sort using the headers, but also when files or directories are being added; - signal connections are not exclusive, and you can even connect the same signal to the same slot/function more than once, with the result that the function will be called as many time as it's been connected; if you're not very careful, you could risk recursion;
- the flag works fine also for empty directories, avoiding the above risk of recursion; if a new root directory is opened and it's empty, it will obviously return an invalid index for the top item (since there's none), but the signal will be disconnected in any case: if another directory (with contents) is opened but not yet loaded, the signal won't be connected again, and if, instead, the contents have been already loaded, it will disconnect it no matter what;
A possibility is to use the Qt.UniqueConnection
connection type and a try
block for the disconnection, but, while this approach works and is consistent, it's a bit cumbersome: as long as the connection is always paired with the setting, using a basic boolean flag is much simpler and easier to read and understand.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.