【问题标题】:QFileSystemModel with Checkboxes带有复选框的 QFileSystemModel
【发布时间】:2020-08-09 03:57:25
【问题描述】:

所以,我有这个简单的 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_())

【问题讨论】:

    标签: python python-3.x pyqt pyqt5 qfilesystemmodel


    【解决方案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_())
    

    【讨论】:

    • 哇!太感谢了!正是我想要的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-19
    • 2010-11-16
    • 2018-02-19
    • 2011-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多