Search code examples
pythonpyside6pyqt6

QListWidget checkbox sync with item selection


I encountered a weird problem when I try to set up a QListWidget where I can click the checkbox to select the items. The current code I wrote won't make this pattern consistent. Like the table below.

enter image description here

For example, when I click the "apple" checkbox, I expect "apple" item will be selected, but sometimes orange" will be deselected. Sometimes it worked the way I want, but it is unpredictable. I have been reading pretty much all StackOverflow threads on this problem, but none solves it. My environment is Mac Big Sur, Python3.7.9, PySide6 6.2.0.

I have an MVE below, could anyone take a look for me? Appreciate it. Stuck here for quite a long time.

from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
import sys


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        self.setWindowTitle("My App")
        self.list = QListWidget(self)
        self.list.setIconSize(QSize(12, 12))

        self.list.setSelectionMode(QAbstractItemView.MultiSelection)
        self.list.itemSelectionChanged.connect(
            self.on_selection_changed)
        self.list.itemChanged.connect(self.on_checkbox_clicked)

        # Set the central widget of the Window.
        self.setCentralWidget(self.list)

        for sheet in ['apple', 'orange', 'banana', 'pearl']:
            item = QListWidgetItem(sheet)
            self.list.addItem(item)
            item.setFlags(item.flags() | Qt.ItemIsUserCheckable)
            item.setCheckState(Qt.Unchecked)

    def on_checkbox_clicked(self, item):
        print(item.text())
        print(item.checkState())
        if item.checkState() is Qt.CheckState.Checked:
            item.setSelected(True)
            # self.list.setCurrentItem(
            #     item, QItemSelectionModel.Select)
            print(f'select: {item.text()}')
        else:
            item.setSelected(False)
            # self.list.setCurrentItem(
            #     item, QItemSelectionModel.Deselect)
            print(f'unselect: {item.text()}')

    def on_selection_changed(self):
        self.list.blockSignals(True)
        for index in range(self.list.count()):
            item = self.list.item(index)
            if item.isSelected():
                item.setCheckState(Qt.CheckState.Checked)
            elif not item.isSelected():
                item.setCheckState(Qt.CheckState.Unchecked)
        self.list.blockSignals(False)


app = QApplication(sys.argv)

window = MainWindow()
window.show()

app.exec()

UPDATED: The first answer is very sophisticated, as I dig more on that there is more to learn. In the end, I found an alternative solution that is much easier to implement. I post the core part for anyone that might need it in the future.

listwidget = QListWidget()
listwidget.setSelectionMode(QAbstractItemView.MultiSelection)

listwidget.itemSelectionChanged.connect(self.on_selection_changed)

def on_selection_changed()
    for index in range(listwidget.count()):
        item = listwidget.item(index)
        if item.isSelected():
            item.setIcon(QIcon("path/to/your/check_icon.png"))
        else:
            item.setIcon(QIcon("path/to/your/uncheck_icon.png"))

Solution

  • From what I understand the OP wants to synchronize that if the user selects an item then it is checked, and vice versa. Considering that, a possible solution is to eliminate the verification that the delegate does on the checkbox rectangle. On the other hand, the user can change the state of the checkbox also using the keyboard, so a possible solution is to use the itemChange signal to update the state of the selection.

    class Delegate(QStyledItemDelegate):
        def editorEvent(self, event, model, option, index):
            last_state = index.data(Qt.ItemDataRole.CheckStateRole)
            ret = super().editorEvent(event, model, option, index)
            if event.type() in (
                QEvent.Type.MouseButtonPress,
                QEvent.Type.MouseButtonDblClick,
            ):
                return False
            elif event.type() == QEvent.Type.MouseButtonRelease and last_state is not None:
                flags = model.flags(index)
                if flags & Qt.ItemFlag.ItemIsUserTristate:
                    state = (last_state + 1) % 3
                else:
                    state = (
                        Qt.CheckState.Unchecked
                        if last_state == Qt.CheckState.Checked
                        else Qt.CheckState.Checked
                    )
                model.setData(index, state, Qt.ItemDataRole.CheckStateRole)
                return False
            return ret
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
    
            self.setWindowTitle("My App")
            self.listwidget = QListWidget(self)
            self.listwidget.setIconSize(QSize(12, 12))
            self.listwidget.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
            self.setCentralWidget(self.listwidget)
            self.listwidget.setItemDelegate(Delegate(self.listwidget))
            self.listwidget.itemChanged.connect(self.handle_itemChanged)
    
            for sheet in ["apple", "orange", "banana", "pearl"]:
                item = QListWidgetItem(sheet)
                self.listwidget.addItem(item)
                item.setFlags(item.flags() | Qt.ItemFlag.ItemIsUserCheckable)
                item.setCheckState(Qt.CheckState.Unchecked)
    
        def handle_itemChanged(self, item):
            item.setSelected(item.checkState() == Qt.CheckState.Checked)