pythonpyqtpyqt4qlistviewqcheckbox

Checkbox selection in QListView


I developed a simple dialog with a checkbox, which allows the user to select one or several items from a list. Besides the standard OK and Cancel buttons, it adds Select All and Unselect All buttons, allowing the user to check/uncheck all items at once (this comes handy for large lists).

import os, sys
from PyQt4 import Qt, QtCore, QtGui

class ChecklistDialog(QtGui.QDialog):
    def __init__(self, name, stringlist=None, checked=False, icon=None, parent=None):
        super(ChecklistDialog, self).__init__(parent)

        self.name = name
        self.icon = icon
        self.model = QtGui.QStandardItemModel()
        self.listView = QtGui.QListView()

        if stringlist is not None:
            for i in range(len(stringlist)):
                item = QtGui.QStandardItem(stringlist[i])
                item.setCheckable(True)
                check = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
                item.setCheckState(check)
                self.model.appendRow(item)

        self.listView.setModel(self.model)

        self.okButton = QtGui.QPushButton("OK")
        self.cancelButton = QtGui.QPushButton("Cancel")
        self.selectButton = QtGui.QPushButton("Select All")
        self.unselectButton = QtGui.QPushButton("Unselect All")

        hbox = QtGui.QHBoxLayout()
        hbox.addStretch(1)
        hbox.addWidget(self.okButton)
        hbox.addWidget(self.cancelButton)
        hbox.addWidget(self.selectButton)
        hbox.addWidget(self.unselectButton)

        vbox = QtGui.QVBoxLayout()
        vbox.addWidget(self.listView)
        vbox.addStretch(1)
        vbox.addLayout(hbox)
        
        self.setLayout(vbox)    
        #self.setLayout(layout)
        self.setWindowTitle(self.name)
        if self.icon is not None: self.setWindowIcon(self.icon)

        self.okButton.clicked.connect(self.accept)
        self.cancelButton.clicked.connect(self.reject)
        self.selectButton.clicked.connect(self.select)
        self.unselectButton.clicked.connect(self.unselect)
        
    def reject(self):
        QtGui.QDialog.reject(self)

    def accept(self):
        self.choices = []
        i = 0
        while self.model.item(i):
            if self.model.item(i).checkState():
                self.choices.append(self.model.item(i).text())
            i += 1
        QtGui.QDialog.accept(self)
        
    def select(self):
        i = 0
        while self.model.item(i):
            item = self.model.item(i)
            if not item.checkState():
                item.setCheckState(True)
            i += 1
        
    def unselect(self):
        i = 0
        while self.model.item(i):
            item = self.model.item(i)
            item.setCheckState(False)
            i += 1  

if __name__ == "__main__":
    fruits = ["Banana", "Apple", "Elderberry", "Clementine", "Fig",
        "Guava", "Mango", "Honeydew Melon", "Date", "Watermelon",
        "Tangerine", "Ugli Fruit", "Juniperberry", "Kiwi", "Lemon",
        "Nectarine", "Plum", "Raspberry", "Strawberry", "Orange"]
    app = QtGui.QApplication(sys.argv)
    form = ChecklistDialog("Fruit", fruits, checked=True)
    if form.exec_():
        print("\n".join([str(s) for s in form.choices]))

The above code works, but I am bothered by the strange behaviour of QListView: when the dialog is display with the "check" parameter turned to True, all checkbozes appear selected with an "X" mark (as expected). However, when the Select All button is clicked, the checkboxes are instead grayed (although the items are correctly selected). I would prefer that they be displayed with an "X" mark, in order to present a consistent appearance to the user.

See the figures below.

Checkbox dialog when first displayed

Checkbox dialog when Select All button is clicked

In general terms, my question is: how to control the way the checkboxes are displayed in a QListView?


Solution

  • The problem is caused because the states of a QCheckBox are 3:

    Qt::Unchecked 0 The item is unchecked.

    Qt::PartiallyChecked 1 The item is partially checked. Items in hierarchical models may be partially checked if some, but not all, of their children are checked.

    Qt::Checked 2 The item is checked.

    And as you see they are integer values, and when you pass the value of True this is converted to 1 which is equivalent to Qt::PartiallyChecked by those you get the rectangle instead of the cross.

    The solution is to pass it the correct value: QtCore.Qt.Checked as I show below:

    import sys
    from PyQt4 import Qt, QtCore, QtGui
    
    
    class ChecklistDialog(QtGui.QDialog):
    
        def __init__(
            self,
            name,
            stringlist=None,
            checked=False,
            icon=None,
            parent=None,
            ):
            super(ChecklistDialog, self).__init__(parent)
    
            self.name = name
            self.icon = icon
            self.model = QtGui.QStandardItemModel()
            self.listView = QtGui.QListView()
    
            for string in stringlist:
                item = QtGui.QStandardItem(string)
                item.setCheckable(True)
                check = \
                    (QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked)
                item.setCheckState(check)
                self.model.appendRow(item)
    
            self.listView.setModel(self.model)
    
            self.okButton = QtGui.QPushButton('OK')
            self.cancelButton = QtGui.QPushButton('Cancel')
            self.selectButton = QtGui.QPushButton('Select All')
            self.unselectButton = QtGui.QPushButton('Unselect All')
    
            hbox = QtGui.QHBoxLayout()
            hbox.addStretch(1)
            hbox.addWidget(self.okButton)
            hbox.addWidget(self.cancelButton)
            hbox.addWidget(self.selectButton)
            hbox.addWidget(self.unselectButton)
    
            vbox = QtGui.QVBoxLayout(self)
            vbox.addWidget(self.listView)
            vbox.addStretch(1)
            vbox.addLayout(hbox)
    
            self.setWindowTitle(self.name)
            if self.icon:
                self.setWindowIcon(self.icon)
    
            self.okButton.clicked.connect(self.onAccepted)
            self.cancelButton.clicked.connect(self.reject)
            self.selectButton.clicked.connect(self.select)
            self.unselectButton.clicked.connect(self.unselect)
    
        def onAccepted(self):
            self.choices = [self.model.item(i).text() for i in
                            range(self.model.rowCount())
                            if self.model.item(i).checkState()
                            == QtCore.Qt.Checked]
            self.accept()
    
        def select(self):
            for i in range(self.model.rowCount()):
                item = self.model.item(i)
                item.setCheckState(QtCore.Qt.Checked)
    
        def unselect(self):
            for i in range(self.model.rowCount()):
                item = self.model.item(i)
                item.setCheckState(QtCore.Qt.Unchecked)
    
    
    if __name__ == '__main__':
        fruits = [
            'Banana',
            'Apple',
            'Elderberry',
            'Clementine',
            'Fig',
            'Guava',
            'Mango',
            'Honeydew Melon',
            'Date',
            'Watermelon',
            'Tangerine',
            'Ugli Fruit',
            'Juniperberry',
            'Kiwi',
            'Lemon',
            'Nectarine',
            'Plum',
            'Raspberry',
            'Strawberry',
            'Orange',
            ]
        app = QtGui.QApplication(sys.argv)
        form = ChecklistDialog('Fruit', fruits, checked=True)
        if form.exec_() == QtGui.QDialog.Accepted:
            print '\n'.join([str(s) for s in form.choices])