3

I wish to create a widget for an app created with PyQt5. I want the user to be able to select any subset of the files within a filesystem hierarchy below a specified directory. I have extended the QFileSystemModel to allow checking the elements in the model roughly following this

example.

I wish the user to be able to modify the checked state of the contents of a directory when the directory is checked - even before the subdirectory has been expanded.

So that this:

image 1

...does this 'under the hood' of the collapsed node in the tree view:

image 2

The problem I am facing is that the QTreeView - and seemingly the QFileSystemModel - are each, or together optimizing performance by only reporting model items which have been viewed already. Until I manually expand the subdirectories in the View, I cannot traverse the data in the Model.

To illustrate, I have added a print callback, and passed it to my (recursive) tree-traversal routine - to print how many children any index has. (See attached code.) Before I expand a subdir in the Tree View by double-clicking on it, it reports no children: Image 1 reports the following when I click on 'one':

tree clicked: /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one
|children|: 0

...but if I expand the view - as in Image 2, Then I see all the children, like this:

tree clicked: /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one
|children|: 7
child[0]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_f
|children|: 0
child[1]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_e
|children|: 0
child[2]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_d
|children|: 0
child[3]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_c
|children|: 0
child[4]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_b
|children|: 0
child[5]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a
|children|: 6
child[0]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/tfd
|children|: 0
child[1]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/sgl
|children|: 0
child[2]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/kjh
|children|: 0
child[3]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/jyk
|children|: 0
child[4]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/dgj
|children|: 0
child[5]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/one_a/..
|children|: 0
child[6]: recursing
traverseDirectory():
model printIndex(): /Users/caleb/dev/ML/cloudburst-ml/data/test_dir/one/..
|children|: 0

How may I induce the loading of the subdirectories, or the expansion of the model's reflection of the actual filesystem? How do I traverse the root path of the QFileSystemModel before the user clicks on the view widget?

Here is my code:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui


class FileTreeSelectorModel(QtWidgets.QFileSystemModel):
    def __init__(self, parent=None, rootpath='/'):
        QtWidgets.QFileSystemModel.__init__(self, None)
        self.root_path      = rootpath
        self.checks         = {}
        self.nodestack      = []
        self.parent_index   = self.setRootPath(self.root_path)
        self.root_index     = self.index(self.root_path)

        self.setFilter(QtCore.QDir.AllEntries | QtCore.QDir.Hidden | QtCore.QDir.NoDot)
        self.directoryLoaded.connect(self._loaded)

    def _loaded(self, path):
        print('_loaded', self.root_path, self.rowCount(self.parent_index))

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role != QtCore.Qt.CheckStateRole:
            return QtWidgets.QFileSystemModel.data(self, index, role)
        else:
            if index.column() == 0:
                return self.checkState(index)

    def flags(self, index):
        return QtWidgets.QFileSystemModel.flags(self, index) | QtCore.Qt.ItemIsUserCheckable

    def checkState(self, index):
        if index in self.checks:
            return self.checks[index]
        else:
            return QtCore.Qt.Checked

    def setData(self, index, value, role):
        if (role == QtCore.Qt.CheckStateRole and index.column() == 0):
            self.checks[index] = value
            print('setData(): {}'.format(value))
            return True
        return QtWidgets.QFileSystemModel.setData(self, index, value, role)

    def traverseDirectory(self, parentindex, callback=None):
        print('traverseDirectory():')
        callback(parentindex)
        if self.hasChildren(parentindex):
            print('|children|: {}'.format(self.rowCount(parentindex)))
            for childRow in range(self.rowCount(parentindex)):
                childIndex = parentindex.child(childRow, 0)
                print('child[{}]: recursing'.format(childRow))
                self.traverseDirectory(childIndex, callback=callback)
        else:
            print('no children')

    def printIndex(self, index):
        print('model printIndex(): {}'.format(self.filePath(index)))


class FileTreeSelectorDialog(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.root_path      = '/Users/caleb/dev/ML/cloudburst-ml/data/test_dir/'

        # Widget
        self.title          = "Application Window"
        self.left           = 10
        self.top            = 10
        self.width          = 1080
        self.height         = 640

        self.setWindowTitle(self.title)         #TODO:  Whilch title?
        self.setGeometry(self.left, self.top, self.width, self.height)

        # Model
        self.model          = FileTreeSelectorModel(rootpath=self.root_path)
        # self.model          = QtWidgets.QFileSystemModel()

        # View
        self.view           = QtWidgets.QTreeView()

        self.view.setObjectName('treeView_fileTreeSelector')
        self.view.setWindowTitle("Dir View")    #TODO:  Which title?
        self.view.setAnimated(False)
        self.view.setIndentation(20)
        self.view.setSortingEnabled(True)
        self.view.setColumnWidth(0,150)
        self.view.resize(1080, 640)

        # Attach Model to View
        self.view.setModel(self.model)
        self.view.setRootIndex(self.model.parent_index)

        # Misc
        self.node_stack     = []

        # GUI
        windowlayout = QtWidgets.QVBoxLayout()
        windowlayout.addWidget(self.view)
        self.setLayout(windowlayout)

        QtCore.QMetaObject.connectSlotsByName(self)

        self.show()

    @QtCore.pyqtSlot(QtCore.QModelIndex)
    def on_treeView_fileTreeSelector_clicked(self, index):
        print('tree clicked: {}'.format(self.model.filePath(index)))
        self.model.traverseDirectory(index, callback=self.model.printIndex)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ex = FileTreeSelectorDialog()
    sys.exit(app.exec_())

I have looked at several links here

[1] - This did not change the behaviour

[2] - This posed an alternative solution which doesn't work for my purposes

[3] - This dealt with the same classes, but not the same issue

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Caleb Howard
  • 45
  • 1
  • 6

1 Answers1

2

By design QFileSystemModel does not load all the items since that task is very heavy, on the other hand hasChildren() indicates if it has children as subdirectories or files but rowCount() only returns the children that are visible due to design issues, that is discussed in this report.

So you should not use rowCount() but do the task of iterating over the directory using QDirIterator:

def traverseDirectory(self, parentindex, callback=None):
    print('traverseDirectory():')
    callback(parentindex)
    if self.hasChildren(parentindex):
        path = self.filePath(parentindex)
        it = QtCore.QDirIterator(path, self.filter()  | QtCore.QDir.NoDotAndDotDot)
        while it.hasNext():
            childIndex =  self.index(it.next())
            self.traverseDirectory(childIndex, callback=callback)
    else:
        print('no children')

I recommend you implement that task in another thread if your model has many levels because it can freeze the GUI.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you, eyllanesc, I had read about this designed feature, and the answer you suggest is not unlike the answer provided in the link ([2]) above. The reason I need to do it within the QFileSystemModel is because it is in my extended class built on QFileSystemModel (FileTreeSelectorModel) that the Checked state is defined, and so I need to traverse the full contents of the directory and set the Checked state within the Model. I suppose I will try doing a QFileSystemModel.index(idx) for idx in the QDirIterator() loop. Is that what you think I should do? Thanks! – Caleb Howard Jul 14 '18 at 15:35
  • While I totally agree with the designed feature to only load needed nodes from the file system, it would add flexibility if QFileSystemModel() had a method by which a single node of the file system could be queried and populated into the model. That would work identically with the current class, and also allow full programmatic knowledge of the file system being modeled by QFileSystemModel. I was assuming the availability of equivalent functionality programmatically as through user input with the mouse. Expanding the view programamatically does not update the Model as I expected it to either. – Caleb Howard Jul 14 '18 at 15:50
  • @CalebHoward If you check my solution what I do is iterate over the children of the folder and get their qmodelindex, and that's what you asked, what answers do you expect? – eyllanesc Jul 14 '18 at 16:09