为 ListView 创建一个切换 "Check All" 复选框

Creating a toggling "Check All" checkbox for a ListView

我有一个充满可检查项的 ListView。我想在 ListView 上方放置一个三态 "check all" 复选框,并且我希望此复选框是双向的。

也就是说,如果用户切换选中所有复选框,我希望 ListView 的所有项目都反映选中所有的 selection。但是,如果用户手动选中或取消选中 ListView 中的项目,我希望 select all 复选框反映该状态(即选中所有 ListView 项目是否已选中,如果全部未选中则未选中,或者部分选中如​​果某些 ListView 项目已选中)。

展示了如何连接第一部分(checking/unchecking select all 框将其状态传播到列表视图的项目)。但是,我不知道如何连接另一个方向。

这就是我如何让选中所有复选框传播到 ListView:

self.layout = QtGui.QVBoxLayout()

self.select_all_cb = QtGui.QCheckBox('Check All', self.ui.tab)
self.select_all_cb.setChecked(True)
self.select_all_cb.setStyleSheet('margin-left: 5px; font: bold')
self.select_all_cb.stateChanged.connect(self.selectAllCheckChanged)
self.layout.addWidget(select_all_cb)

self.listview = QtGui.QListView(self.ui.tab)
self.listview.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
self.listview.setSelectionRectVisible(False)

model = QStandardItemModel()
for checkItem in self.checkItems:
    item = QStandardItem(checkItem)
    item.setCheckable(True)
    item.setSelectable(False)
    item.setCheckState(QtCore.Qt.Checked)
    model.appendRow(item)
self.listview.setModel(model)
self.layout.addWidget(listview)


def selectAllCheckChanged(self):
    model = self.listview.model()
    for index in range(model.rowCount()):
        item = model.item(index)
        if item.isCheckable():
            if self.select_all_cb.isChecked():
                item.setCheckState(QtCore.Qt.Checked)
            else:
                item.setCheckState(QtCore.Qt.Unchecked)

关于如何走另一条路有什么建议吗?

您可以连接到 QStandardItemModel 上的 itemChanged 信号并测试所有复选框的状态。

from itertools import product

self.model.itemChanged.connect(self.test_check)

def test_check(self, item):
    items = [self.model.item(r,c) for r, c in product(range(self.model.rowCount()), range(self.model.columnCount())]

    if all(item.checkState() == Qt.Checked for item in items)
        state = Qt.Checked
    elif any(item.checkState() == Qt.Checked for item in items):
        state = Qt.PartiallyChecked
    else:
        state = Qt.Unchecked

    if self.select_all_cb.checkState() != state:
        self.select_all_cb.setCheckState(state)

如果您有大量的复选框,您可以通过缓存每个项目的检查状态并在项目状态发生变化时更新缓存来优化这一点,然后检查缓存而不是从中提取它每个项目每次。

如果您知道您将同时对许多项目进行更改,您可能应该在模型上使用 blockSignals,然后 运行 在完成所有更改后手动执行此功能。

在您的 selectAllCheckChanged 处理程序中,您还应该阻止模型上的信号,这样它就不会触发此处理程序

def selectAllCheckChanged(self):
    model = self.listview.model()
    model.blockSignals(True)
    try:
        for index in range(model.rowCount()):
            item = model.item(index)
            if item.isCheckable():
                if self.select_all_cb.isChecked():
                    item.setCheckState(QtCore.Qt.Checked)
                else:
                    item.setCheckState(QtCore.Qt.Unchecked)
    finally:
        model.blockSignals(False)

为了防止对其他人有所帮助,以下是我将 Brendan 的回答合并到我的代码中的方法。不同之处在于三态功能仅在需要时启用(因此用户无法启用部分检查状态),我用 clicked 信号而不是 stateChange 连接它以避免 selectAllCheckChanged 不会被 listviewCheckChanged 触发。 model.blockSignals 当然也可以,但是使用 clicked 对我来说似乎更符合 pythonic。

self.layout = QtGui.QVBoxLayout()

self.select_all_cb = QtGui.QCheckBox('Check All', self.ui.tab)
self.select_all_cb.setTristate(False) # Only enable tristate when necessary so the user doesn't click it through to partially checked
self.select_all_cb.setChecked(True)
self.select_all_cb.setStyleSheet('margin-left: 5px; font: bold')
self.select_all_cb.clicked.connect(self.selectAllCheckChanged) # clicked instead of stateChanged so this doesn't get triggered by ListView's changes
self.layout.addWidget(select_all_cb)

self.listview = QtGui.QListView(self.ui.tab)
self.listview.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.listview.setSelectionMode(QtGui.QAbstractItemView.NoSelection)
self.listview.setSelectionRectVisible(False)

model = QStandardItemModel()
for checkItem in self.checkItems:
    item = QStandardItem(checkItem)
    item.setCheckable(True)
    item.setSelectable(False)
    item.setCheckState(QtCore.Qt.Checked)
    model.appendRow(item)
self.listview.setModel(model)
self.listview.clicked.connect(self.listviewCheckChanged)
self.layout.addWidget(listview)


def selectAllCheckChanged(self):
    ''' updates the listview based on select all checkbox '''
    model = self.listview.model()
    for index in range(model.rowCount()):
        item = model.item(index)
        if item.isCheckable():
            if self.select_all_cb.isChecked():
                item.setCheckState(QtCore.Qt.Checked)
            else:
                item.setCheckState(QtCore.Qt.Unchecked)

def listviewCheckChanged(self):
    ''' updates the select all checkbox based on the listview '''
    model = self.listview.model()
    items = [model.item(index) for index in range(model.rowCount())]

    if all(item.checkState() == QtCore.Qt.Checked for item in items):
        self.select_all_cb.setTristate(False)
        self.select_all_cb.setCheckState(QtCore.Qt.Checked)
    elif any(item.checkState() == QtCore.Qt.Checked for item in items):
        self.select_all_cb.setTristate(True)
        self.select_all_cb.setCheckState(QtCore.Qt.PartiallyChecked)
    else:
        self.select_all_cb.setTristate(False)
        self.select_all_cb.setCheckState(QtCore.Qt.Unchecked)