Evaine LeBlanc
Evaine LeBlanc

Reputation: 81

Selecting the top-most item of a QTreeView

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_())

Upvotes: 0

Views: 785

Answers (1)

musicamante
musicamante

Reputation: 48260

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 get None);
  • 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 the directoryLoaded for the "documents" folder, which will then call set_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.

Upvotes: 1

Related Questions