QComboBox with checkboxes



  • I need a combobox which allows user to select multiple items. I created custom model and QComboBox subclass which uses that model. Here is my code

    class CheckableItemsModel(QStandardItemModel):
    
        checkStateChanged = pyqtSignal()
    
        def __init__(self, parent=None):
            super(CheckableItemsModel, self).__init__(parent)
    
        def flags(self, index):
            return super(CheckableItemsModel, self).flags(index) | Qt.ItemIsUserCheckable
    
        def data(self, index, role=Qt.DisplayRole):
            value = super(CheckableItemsModel, self).data(index, role)
            if index.isValid() and role == Qt.CheckStateRole and value is None:
                value = Qt.Unchecked
            return value
    
        def setData(self, index, value, role=Qt.EditRole):
            ok = super(CheckableItemsModel, self).setData(index, value, role)
            if ok and role == Qt.CheckStateRole:
                self.checkStateChanged.emit()
    
            return ok
    
    
    class CheckComboBox(QComboBox):
    
        checkedItemsChanged = pyqtSignal(list)
    
        def __init__(self, parent=None):
            super(CheckComboBox, self).__init__(parent)
    
            # workaround for Mac and GTK to show checkboxes
            self.setStyleSheet('QComboBox { combobox-popup: 1px }')
    
            self.defaultText = ''
            self.separator = ','
            self.containerPressed = False
    
            self.checkableModel = CheckableItemsModel(self)
            self.setModel(self.checkableModel)
    
            self.model().checkStateChanged.connect(self.updateCheckedItems)
            self.model().rowsInserted.connect(self.updateCheckedItems)
            self.model().rowsRemoved.connect(self.updateCheckedItems)
    
            self.activated.connect(self.toggleCheckState)
    
        def itemCheckState(self, index):
            return self.itemData(index, Qt.CheckStateRole)
    
        def setItemCheckState(self, index, state):
            self.setItemData(index, state, Qt.CheckStateRole)
    
        def checkedItems(self):
            items = list()
            if self.model():
                index = self.model().index(0, self.modelColumn(), self.rootModelIndex())
                indexes = self.model().match(index, Qt.CheckStateRole, Qt.Checked, -1, Qt.MatchExactly)
                for i in indexes:
                    items.append(index.data())
    
            return items
    
        def setCheckedItems(self, items):
            for i in items:
                index = self.findText(i)
                self.setItemCheckState(index, Qt.Checked if index != -1 else Qt.Unchecked)
    
        def updateCheckedItems(self):
            items = self.checkedItems()
            if len(items) == 0:
                self.setEditText(self.defaultText)
            else:
                self.setEditText(self.separator.join(items))
    
            self.checkedItemsChanged.emit(items)
    
        def toggleCheckState(self, index):
            value = self.itemData(index, Qt.CheckStateRole)
            if value is not None:
                self.setItemData(index, Qt.Checked if value == Qt.Unchecked else Qt.Unchecked, Qt.CheckStateRole)
    

    With this code I have combobox with checkable items. But there are two problems:

    • I can't select multiple items. When I click (check) one item, dropdown list closed. I suspect that some event filtering should help.
    • in Mac and GTK environments checkboxes are not visible

    Any ideas how to fix these two issues? Thanks in advance.


  • Moderators

    @voltron For such a use case it would be better to use a QListBox



  • @jsulm I know about QListWidget/QListView, but I need combobox with multiple selection not QListWidget/QListView. If I could use them, I definitely won't bother with developing custom widget.



  • @voltron

    the problem is the Activated Signal that is emited as soon as you select a new entry from the Combobox. You would have to overwrite the functions that emit the signal and I believe most of those are private.

    The simples solution is to block the Signals and somehow define your own "I'm done with selection"-condition.



  • @voltron You decide, of course, but the purpose of combobox is to let the user select one option out of many. User interface controls should not be used against their intended meaning to ensure consistent user experience and intuitiveness. Such selection should be done with a button and popup menu or something like that. In a combobox the selected item is visible in the basic state and it's not possible with multiple selection.



  • @J.Hilk thanks for the hint. I implemented event filter, so popup does not closed after selecting single item, updated code below. But still no luck with multiple selection, seems something wrong with model too.

    class CheckableItemsModel(QStandardItemModel):
    
        checkStateChanged = pyqtSignal()
    
        def __init__(self, parent=None):
            super(CheckableItemsModel, self).__init__(parent)
    
        def flags(self, index):
            return super(CheckableItemsModel, self).flags(index) | Qt.ItemIsUserCheckable
    
        def data(self, index, role=Qt.DisplayRole):
            value = super(CheckableItemsModel, self).data(index, role)
            if index.isValid() and role == Qt.CheckStateRole and value is None:
                value = Qt.Unchecked
            return value
    
        def setData(self, index, value, role=Qt.EditRole):
            ok = super(CheckableItemsModel, self).setData(index, value, role)
            if ok and role == Qt.CheckStateRole:
                self.checkStateChanged.emit()
    
            return ok
    
    
    class CheckComboBox(QComboBox):
    
        checkedItemsChanged = pyqtSignal(list)
    
        def __init__(self, parent=None):
            super(CheckComboBox, self).__init__(parent)
    
            self.defaultText = ''
            self.separator = ','
            self.containerMousePress = False
    
            self.checkableModel = CheckableItemsModel(self)
            self.setModel(self.checkableModel)
    
            self.model().checkStateChanged.connect(self.updateCheckedItems)
            self.model().rowsInserted.connect(self.updateCheckedItems)
            self.model().rowsRemoved.connect(self.updateCheckedItems)
    
            self.activated.connect(self.toggleCheckState)
    
        def itemCheckState(self, index):
            return self.itemData(index, Qt.CheckStateRole)
    
        def setItemCheckState(self, index, state):
            self.setItemData(index, state, Qt.CheckStateRole)
    
        def checkedItems(self):
            items = list()
            if self.model():
                index = self.model().index(0, self.modelColumn(), self.rootModelIndex())
                indexes = self.model().match(index, Qt.CheckStateRole, Qt.Checked, -1, Qt.MatchExactly)
                for i in indexes:
                    items.append(index.data())
    
            return items
    
        def setCheckedItems(self, items):
            for i in items:
                index = self.findText(i)
                self.setItemCheckState(index, Qt.Checked if index != -1 else Qt.Unchecked)
    
        def updateCheckedItems(self):
            items = self.checkedItems()
            if len(items) == 0:
                self.setEditText(self.defaultText)
            else:
                self.setEditText(self.separator.join(items))
    
            self.checkedItemsChanged.emit(items)
    
        def toggleCheckState(self, index):
            value = self.itemData(index, Qt.CheckStateRole)
            if value is not None:
                self.setItemData(index, Qt.Checked if value == Qt.Unchecked else Qt.Unchecked, Qt.CheckStateRole)
    
        def eventFilter(self, receiver, event):
            eventType = event.type()
            if eventType in [QEvent.KeyPress, QEvent.KeyRelease]:
                if receiver == self and event.key() in [Qt.Key_Up, Qt.Key_Down]:
                    self.showPopup()
                    return True
                elif event.key() in [Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape]:
                    self.hidePopup()
                    if event.key() != Qt.Key_Escape:
                        return True
            elif eventType == QEvent.MouseButtonPress:
                self.containerMousePress = receiver == self.view().window()
            elif eventType == QEvent.MouseButtonRelease:
                self.containerMousePress = False
    
            return False
    
        def hidePopup(self):
            if self.containerMousePress:
                super(CheckComboBox, self).hidePopup()
    
    

  • Lifetime Qt Champion

    Hi,

    QComboBox uses a QAbstractItemView based widget for its view so you can modify the selection mode property of it.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.