Search code examples
qtpyqt6qlistwidget

Why can't the checkbox of QListWidgetItem be clicked normally after I connect the ItemClicked signal of QListwidget to a slot function?


I implemented an optional list through QListWidget, and then I found that if I want to change the state of the checkbox, I can only change it by clicking the checkbox.

But I also wanted to switch the checkbox state by clicking on the Item, so I connected the ItemClicked signal of QListWidget to a new slot function to implement it.

I later discovered that the state of the checkbox associated with the Item can only be changed by clicking on the Item, not the checkbox itself.

I'm curious about what's going on in this process, and what I should do to achieve my goal, which is that the state of the checkbox can be switched by clicking on the checkbox or the item itself. Blow is my code:

class Item(QListWidgetItem):
    def __init__(self):
        super().__init__()
        # self.setFlags(Qt.ItemFlag.ItemIsUserCheckable | Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsEnabled)
        self.setCheckState(Qt.CheckState.Unchecked)
        self.setToolTip("double click edit")


class MainWindow(QWidget):
    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        self.add_btn = QPushButton("add")
        self.select_all = QPushButton("select all")
        self.del_btn = QPushButton("delete")
        btn_layout = QHBoxLayout()
        btn_layout.addWidget(self.select_all)
        btn_layout.addWidget(self.add_btn)
        btn_layout.addWidget(self.del_btn)
        self.list_widget = QListWidget()
        self.list_widget.setSelectionMode(QListWidget.SelectionMode.ContiguousSelection)
        self.list_widget.setAlternatingRowColors(True)
        layout.addLayout(btn_layout)
        layout.addWidget(self.list_widget)

        self.setLayout(layout)
        self.add_btn.clicked.connect(self.addItem)
        # self.list_widget.itemClicked.connect(self.itemClickedEvent)

    def addItem(self):
        item = Item()
        item.setText("test")
        self.list_widget.addItem(item)

    def itemClickedEvent(self, item: QListWidgetItem):
        pass
        if item.checkState() == Qt.CheckState.Checked:
            item.setCheckState(Qt.CheckState.Unchecked)
        else:
            item.setCheckState(Qt.CheckState.Checked)

Solution

  • This is caused by the fact that, when attempting to toggle the checkbox of the item, it's also being clicked, so the check state is actually changed twice.

    You can verify this by connecting to the itemChanged signal:

    self.list_widget.itemChanged.connect(lambda i: print(i.checkState()))
    

    When you click on the check indicator, the signal is emitted twice, the first time with the new check state, and the second one with the previous state, set by your function.

    A possible solution is to make the item not user checkable:

    class Item(QListWidgetItem):
        def __init__(self):
            super().__init__()
            self.setCheckState(Qt.CheckState.Unchecked)
            self.setFlags(self.flags() & ~Qt.ItemFlag.ItemIsUserCheckable)
            self.setToolTip("double click edit")
    

    Unfortunately, this has the disadvantage of making it impossible to toggle the state by using the keyboard.

    As an alternative, you can use a custom delegate and make it ignore the left mouse button release whenever it happens within the check indicator rectangle. In this case you must not change the item flags as explained above.

    class Delegate(QStyledItemDelegate):
        _checkFlags = Qt.ItemFlag.ItemIsEnabled | Qt.ItemFlag.ItemIsUserCheckable
        def editorEvent(self, event, model, option, index):
            if (
                event.type() == event.Type.MouseButtonRelease
                and event.button() == Qt.MouseButton.LeftButton
                and index.flags() & self._checkFlags == self._checkFlags
                and option.state & QStyle.State.State_Enabled
            ):
                opt = QStyleOptionViewItem(option)
                self.initStyleOption(opt, index)
                checkRect = option.widget.style().subElementRect(
                    QStyle.SubElement.SE_ItemViewItemCheckIndicator, opt, option.widget)
                if event.pos() in checkRect:
                    return False
            return super().editorEvent(event, model, option, index)
    
    
    ...
    self.list_widget.setItemDelegate(Delegate(self.list_widget))