QFileSystemModel с флажками

Итак, у меня есть этот простой код PyQt5, который по сути является файловым менеджером. Мне нужно иметь возможность выбирать произвольные файлы или группы файлов (каталоги и все дочерние). Я хотел бы:

  1. Добавьте флажок рядом с каждым элементом
  2. Если элемент отмечен/снят с отметки и имеет подэлементы, состояние подэлементов должно быть установлено в состояние элемента. Поэтому, если вы проверяете каталог, все под ним также должно быть проверено.
  3. Когда состояние проверки элементов изменяется прямо или косвенно, мне нужно вызвать обратный вызов с полным путем (относительно корня) элемента.

По сути, я создаю список выбранных файлов для обработки.

import sys
from PyQt5.QtWidgets import QApplication, QFileSystemModel, QTreeView, QWidget, QVBoxLayout
from PyQt5.QtGui import QIcon

class App(QWidget):

    def __init__(self):
        super().__init__()
        self.title = 'PyQt5 file system view - pythonspot.com'
        self.left = 10
        self.top = 10
        self.width = 640
        self.height = 480
        self.initUI()
    
    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)
        
        self.model = QFileSystemModel()
        self.model.setRootPath('')
        self.tree = QTreeView()
        self.tree.setModel(self.model)
        
        self.tree.setAnimated(False)
        self.tree.setIndentation(20)
        self.tree.setSortingEnabled(True)
        
        self.tree.setWindowTitle("Dir View")
        self.tree.resize(640, 480)
        
        windowLayout = QVBoxLayout()
        windowLayout.addWidget(self.tree)
        self.setLayout(windowLayout)
        
        self.show()

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

person Kory    schedule 07.08.2020    source источник


Ответы (1)


QFileSystemModel не загружает содержимое каталога до тех пор, пока не будет явно запрошено (в случае древовидного представления это происходит только при первом раскрытии каталога).

Это требует тщательной проверки и рекурсивной установки состояния проверки каждого пути не только при добавлении (или переименовании/удалении) нового файла или каталога, но и при фактической загрузке содержимого каталога.

Чтобы правильно реализовать это, состояния проверки также должны храниться с использованием путей к файлам, потому что при изменении содержимого каталога некоторые индексы могут стать недействительными.

Следующая реализация должна позаботиться обо всем, что написано выше, и генерировать сигнал только тогда, когда состояние элемента активно изменяется и изменяется родительское состояние, но нет для дочерних элементов. проверенного каталога.
Хотя этот выбор может показаться частично бессвязным, это требование производительности, так как вы не можете получить отдельные сигналы для каждого подкаталога (и, возможно, захотите): если вы проверите каталог верхнего уровня, вы можете получать тысячи нежелательных уведомлений; с другой стороны, может быть важно получать уведомление, если состояние родительского каталога изменилось, когда все элементы становятся отмеченными или не отмеченными.

from PyQt5 import QtCore, QtWidgets

class CheckableFileSystemModel(QtWidgets.QFileSystemModel):
    checkStateChanged = QtCore.pyqtSignal(str, bool)
    def __init__(self):
        super().__init__()
        self.checkStates = {}
        self.rowsInserted.connect(self.checkAdded)
        self.rowsRemoved.connect(self.checkParent)
        self.rowsAboutToBeRemoved.connect(self.checkRemoved)

    def checkState(self, index):
        return self.checkStates.get(self.filePath(index), QtCore.Qt.Unchecked)

    def setCheckState(self, index, state, emitStateChange=True):
        path = self.filePath(index)
        if self.checkStates.get(path) == state:
            return
        self.checkStates[path] = state
        if emitStateChange:
            self.checkStateChanged.emit(path, bool(state))

    def checkAdded(self, parent, first, last):
        # if a file/directory is added, ensure it follows the parent state as long
        # as the parent is already tracked; note that this happens also when 
        # expanding a directory that has not been previously loaded
        if not parent.isValid():
            return
        if self.filePath(parent) in self.checkStates:
            state = self.checkState(parent)
            for row in range(first, last + 1):
                index = self.index(row, 0, parent)
                path = self.filePath(index)
                if path not in self.checkStates:
                    self.checkStates[path] = state
        self.checkParent(parent)

    def checkRemoved(self, parent, first, last):
        # remove items from the internal dictionary when a file is deleted; 
        # note that this *has* to happen *before* the model actually updates, 
        # that's the reason this function is connected to rowsAboutToBeRemoved
        for row in range(first, last + 1):
            path = self.filePath(self.index(row, 0, parent))
            if path in self.checkStates:
                self.checkStates.pop(path)

    def checkParent(self, parent):
        # verify the state of the parent according to the children states
        if not parent.isValid():
            return
        childStates = [self.checkState(self.index(r, 0, parent)) for r in range(self.rowCount(parent))]
        newState = QtCore.Qt.Checked if all(childStates) else QtCore.Qt.Unchecked
        oldState = self.checkState(parent)
        if newState != oldState:
            self.setCheckState(parent, newState)
            self.dataChanged.emit(parent, parent)
        self.checkParent(parent.parent())

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

    def data(self, index, role=QtCore.Qt.DisplayRole):
        if role == QtCore.Qt.CheckStateRole and index.column() == 0:
            return self.checkState(index)
        return super().data(index, role)

    def setData(self, index, value, role, checkParent=True, emitStateChange=True):
        if role == QtCore.Qt.CheckStateRole and index.column() == 0:
            self.setCheckState(index, value, emitStateChange)
            for row in range(self.rowCount(index)):
                # set the data for the children, but do not emit the state change, 
                # and don't check the parent state (to avoid recursion)
                self.setData(index.child(row, 0), value, QtCore.Qt.CheckStateRole, 
                    checkParent=False, emitStateChange=False)
            self.dataChanged.emit(index, index)
            if checkParent:
                self.checkParent(index.parent())
            return True

        return super().setData(index, value, role)


class Test(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QVBoxLayout(self)

        self.tree = QtWidgets.QTreeView()
        layout.addWidget(self.tree, stretch=2)

        model = CheckableFileSystemModel()
        model.setRootPath('')
        self.tree.setModel(model)
        self.tree.setSortingEnabled(True)
        self.tree.header().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)

        self.logger = QtWidgets.QPlainTextEdit()
        layout.addWidget(self.logger, stretch=1)
        self.logger.setReadOnly(True)

        model.checkStateChanged.connect(self.updateLog)
        self.resize(640, 480)
        QtCore.QTimer.singleShot(0, lambda: self.tree.expand(model.index(0, 0)))

    def updateLog(self, path, checked):
        if checked:
            text = 'Path "{}" has been checked'
        else:
            text = 'Path "{}" has been unchecked'
        self.logger.appendPlainText(text.format(path))
        self.logger.verticalScrollBar().setValue(
            self.logger.verticalScrollBar().maximum())


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    test = Test()
    test.show()
    sys.exit(app.exec_())
person musicamante    schedule 08.08.2020
comment
Ух ты! Большое спасибо! Именно то, что я искал. - person Kory; 10.08.2020
comment
Пожалуйста. Помните, что если ответ поможет вам решить вашу проблему, вы должны пометить его как принятый, щелкнув символ проверки на слева от него. - person musicamante; 10.08.2020