Unsolved 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.
-
@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 notQListWidget
/QListView
. If I could use them, I definitely won't bother with developing custom widget. -
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()
-
Hi,
QComboBox uses a QAbstractItemView based widget for its view so you can modify the selection mode property of it.