带有复选框的 QFileSystemModel
QFileSystemModel with Checkboxes
所以,我有这个简单的 PyQt5 代码,它本质上是一个文件浏览器。我需要能够 select 任意文件或文件组(目录和所有子文件)。我愿意:
- 在每个项目旁边添加一个复选框
- 如果一个项目是 checked/uncheck 并且有子项目,子项目的状态应该设置为项目的状态。所以如果你检查一个目录,它下面的所有东西也应该被检查。
- 当项目检查状态直接或间接更改时,我需要使用项目的完整路径(相对于根)调用回调。
我实际上是在构建要处理的 selected 文件列表。
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_())
QFileSystemModel 在明确请求之前不会加载目录的内容(在树视图的情况下,它只会在目录第一次展开时发生)。
这需要仔细验证并递归设置每个路径的检查状态,不仅在添加新文件或目录时(或renamed/removed),而且在实际加载目录内容时。
为了正确实现这一点,还应使用文件路径存储检查状态,因为当目录内容更改时,某些索引可能会失效。
下面的实现应该处理上面写的所有内容,并且只有当项目状态被主动改变时才会发出信号并且父状态被改变,但是not 用于已检查目录的子项目。
虽然这种选择可能看起来有些不连贯,但它是一项性能要求,因为您无法获得每个子目录的单独信号(您可能也不想):如果您检查顶级目录,您可能会收到数以千计的不需要的通知;另一方面,如果父目录状态已更改,无论何时所有项目都被选中或未选中,接收通知可能很重要。
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_())
所以,我有这个简单的 PyQt5 代码,它本质上是一个文件浏览器。我需要能够 select 任意文件或文件组(目录和所有子文件)。我愿意:
- 在每个项目旁边添加一个复选框
- 如果一个项目是 checked/uncheck 并且有子项目,子项目的状态应该设置为项目的状态。所以如果你检查一个目录,它下面的所有东西也应该被检查。
- 当项目检查状态直接或间接更改时,我需要使用项目的完整路径(相对于根)调用回调。
我实际上是在构建要处理的 selected 文件列表。
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_())
QFileSystemModel 在明确请求之前不会加载目录的内容(在树视图的情况下,它只会在目录第一次展开时发生)。
这需要仔细验证并递归设置每个路径的检查状态,不仅在添加新文件或目录时(或renamed/removed),而且在实际加载目录内容时。
为了正确实现这一点,还应使用文件路径存储检查状态,因为当目录内容更改时,某些索引可能会失效。
下面的实现应该处理上面写的所有内容,并且只有当项目状态被主动改变时才会发出信号并且父状态被改变,但是not 用于已检查目录的子项目。
虽然这种选择可能看起来有些不连贯,但它是一项性能要求,因为您无法获得每个子目录的单独信号(您可能也不想):如果您检查顶级目录,您可能会收到数以千计的不需要的通知;另一方面,如果父目录状态已更改,无论何时所有项目都被选中或未选中,接收通知可能很重要。
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_())