QTreeWidget(如 QListWidget 和 QTableWidget)有其内部模型;它是对数据模型的某种高级访问,并且其实际模型不是直接的(如easily)可以访问,也不应该。它们是“简化的”模型视图界面,旨在用于不需要高级编辑的一般用途,但最重要的是它们只支持他们自己的,single和独特的模型。除了 Q[viewType]WidgetItem 接口之外,没有简单的方法可以更改它,除非您完全重置模型,这意味着如果您想在同一视图中使用多个模型,则需要将数据“存储”在其他地方,使整个事情比需要的更加复杂,并且很容易出现错误和问题,这正是您的情况所发生的情况。
另一方面,这些 QWidgetItemView 提供了标准模型和视图中缺少的一些功能,其中之一是 QTreeWidget 中项目的“自动检查”。
虽然这个功能非常有用,但它可能是一个系列PITA当您需要在同一视图上显示不同的数据模型时;这意味着,为了避免重新发明轮子,最好坚持使用 QTreeView/QStandardItemModel 对并仅实现三态机制,而不是使用可能与 QTreeWidget 的内部实现发生冲突的复杂方法。
分离QStandardItemModel
子类实例,具有父/子三态支持
这里最重要的方面是您将使用single数据模型类实例对于每个数据集(而不是多个字典+视图的模型对),只需简单地轻按一下即可在它们之间切换变得更加容易setModel()
.
缺点是前面提到的缺乏父母/孩子国家的支持,这是必须实施的;一旦这个逻辑得到解决,你就会得到多个持久、独特、一致模型,无论您实际需要多少个。
除了实际的模型内容初始化之外,您只需要子类化以下两个方法QStandardItemModel
:
-
setData(index, value, role)
is overridden to apply the check state to the children indexes: if the role is Qt.CheckState
and the index has any children, the [un]checked state is applied to them; if the index has a parent, the index emits the dataChanged
signal to the model, ensuring that its view[s] requires updates (otherwise the checkbox visible state won't be updated correctly until the view is repainted)[1];
-
data(index, role)
需要覆盖"show"父级的检查状态;模型的索引数据是什么并不重要:如果它有任何子项,则其状态完全取决于它们(全部/任何/无检查),否则它基于默认模型索引的 checkState;
一旦解决了这个问题,您只需关心将新选择的模型设置到视图中,并且所有状态都将像切换到另一个模型(如果有)之前一样存在。
为了与您的示例保持一致,我使用了基于字典的模型数据创建逻辑,但我建议您使用递归方法来添加子子项。
因为我已经在那里了,所以我还添加了一种机制来存储每个索引的扩展状态,以实现更好的视图/模型一致性;这不是必需的,但它确实有助于用户体验:-) 请记住,这只是为了演示目的:显然,如果您添加/删除项目而不考虑内部 ExpandState 字典,则这将无法正常工作(或赢得根本不起作用!)。
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
dataSets = [
{
"itemA" :{
"menuA": ["a101", "a102"],
},
"itemBC": {
"menuC": ["c101", "c102", "c103"],
"menuB": ["b101"]
},
},
{
"itemD" :{
"menuD": ["d100"],
},
}
]
class TreeModel(QtGui.QStandardItemModel):
checkStateChange = QtCore.pyqtSignal(QtCore.QModelIndex, bool)
def __init__(self, dataSet):
super(TreeModel, self).__init__()
# unserialize data, as per your original code; you might want to use a
# recursive function instead, to allow multiple levels of items
for page_name, page_contents in dataSet.items():
for pk, pv in page_contents.items():
parent = QtGui.QStandardItem(pk)
parent.setCheckable(True)
self.appendRow(parent)
if pv:
parent.setTristate(True)
for c in pv:
child = QtGui.QStandardItem(c)
child.setCheckable(True)
parent.appendRow(child)
self.dataChanged.connect(self.checkStateChange)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.CheckStateRole:
childState = QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
# set all children states according to this parent item
for row in range(self.rowCount(index)):
for col in range(self.columnCount(index)):
childIndex = self.index(row, col, index)
self.setData(childIndex, childState, QtCore.Qt.CheckStateRole)
# if the item has a parent, emit the dataChanged signal to ensure
# that the parent state is painted correctly according to what data()
# will return; note that this will emit the dataChanged signal whatever
# the "new" parent state is, meaning that it might still be the same
parent = self.parent(index)
if parent.isValid():
self.dataChanged.emit(parent, parent)
return super(TreeModel, self).setData(index, value, role)
def data(self, index, role=QtCore.Qt.DisplayRole):
# QStandardItemModel doesn't support auto tristate based on its children
# as it does for QTreeWidget's internal model; we have to implement that
if role == QtCore.Qt.CheckStateRole and self.flags(index) & QtCore.Qt.ItemIsTristate:
childStates = []
# collect all child check states
for row in range(self.rowCount(index)):
for col in range(self.columnCount(index)):
childIndex = self.index(row, col, index)
childState = self.data(childIndex, QtCore.Qt.CheckStateRole)
# if the state of a children is partially checked we can
# stop here and return a partially checked state
if childState == QtCore.Qt.PartiallyChecked:
return QtCore.Qt.PartiallyChecked
childStates.append(childState)
if all(childStates):
# all children are checked, yay!
return QtCore.Qt.Checked
elif any(childStates):
# only some children are checked...
return QtCore.Qt.PartiallyChecked
# no item is checked, so bad :-(
return QtCore.Qt.Unchecked
return super(TreeModel, self).data(index, role)
def checkStateChange(self, topLeft, bottomRight):
# if you need some control back to your data outside the model, here is
# the right place to do it; note that *usually* the topLeft and
# bottomRight indexes are the same, expecially with QStandardItemModels
# but that would not be the same in some special cases
pass
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.treeView = QtWidgets.QTreeView()
layout.addWidget(self.treeView)
self.models = []
self.expandStates = {}
for i, dataSet in enumerate(dataSets):
model = TreeModel(dataSet)
button = QtWidgets.QPushButton('Data-{:02}'.format(i + 1))
layout.addWidget(button)
button.clicked.connect(lambda _, model=model: self.setModel(model))
def getExpandState(self, expDict, model, index=QtCore.QModelIndex()):
# set the index expanded state, if it's not the root index:
# the root index is not a valid index!
if index.isValid():
expDict[index] = self.treeView.isExpanded(index)
# if the index (or root index) has children, set their states
for row in range(model.rowCount(index)):
for col in range(model.columnCount(index)):
childIndex = model.index(row, col, index)
# if the current index has children, set their expand state
# using this function, which is recursive
for childRow in range(model.rowCount(childIndex)):
self.getExpandState(expDict, model, childIndex)
def setModel(self, model):
if self.treeView.model():
if self.treeView.model() == model:
# the model is the same, no need to update anything
return
# save the expand states of the current model before changing it
prevModel = self.treeView.model()
self.expandStates[prevModel] = expDict = {}
self.getExpandState(expDict, prevModel)
self.treeView.setModel(model)
if model in self.expandStates:
# if the new model has expand states saved, restore them
for index, expanded in self.expandStates.get(model, {}).items():
self.treeView.setExpanded(index, expanded)
else:
self.treeView.expandAll()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Window()
w.show()
sys.exit(app.exec_())
[1]: In this example the dataChanged
signal is emitted whenever any child item check state changes. This isn't a big issue, but if you really need to avoid unnecessary dataChanged notifications you might need to add a QtCore.QTimer.singleshot
delayed dataChanged signal emission only if the parent state has changed. It's not that hard to achieve, but I didn't think it was really necessary for this example.