from PyQt5.QtWidgets import QComboBox, QStyledItemDelegate, qApp from PyQt5.QtCore import Qt, QEvent from PyQt5.QtGui import QPalette, QStandardItem, QFontMetrics class CheckableComboBox(QComboBox): # Subclass Delegate to increase item height class Delegate(QStyledItemDelegate): def sizeHint(self, option, index): size = super().sizeHint(option, index) size.setHeight(20) return size def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Make the combo editable to set a custom text, but readonly self.setEditable(True) self.lineEdit().setReadOnly(True) # Make the lineedit the same color as QPushButton palette = qApp.palette() palette.setBrush(QPalette.Base, palette.button()) self.lineEdit().setPalette(palette) # Use custom delegate self.setItemDelegate(CheckableComboBox.Delegate()) # Update the text when an item is toggled self.model().dataChanged.connect(self.updateText) # Hide and show popup when clicking the line edit self.lineEdit().installEventFilter(self) self.closeOnLineEditClick = False # Prevent popup from closing when clicking on an item self.view().viewport().installEventFilter(self) def resizeEvent(self, event): # Recompute text to elide as needed self.updateText() super().resizeEvent(event) def eventFilter(self, object, event): if object == self.lineEdit(): if event.type() == QEvent.MouseButtonRelease: if self.closeOnLineEditClick: self.hidePopup() else: self.showPopup() return True return False if object == self.view().viewport(): if event.type() == QEvent.MouseButtonRelease: index = self.view().indexAt(event.pos()) item = self.model().item(index.row()) if item.checkState() == Qt.Checked: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Checked) return True return False def showPopup(self): super().showPopup() # When the popup is displayed, a click on the lineedit should close it self.closeOnLineEditClick = True def hidePopup(self): super().hidePopup() # Used to prevent immediate reopening when clicking on the lineEdit self.startTimer(100) # Refresh the display text when closing self.updateText() def timerEvent(self, event): # After timeout, kill timer, and reenable click on line edit self.killTimer(event.timerId()) self.closeOnLineEditClick = False def updateText(self): texts = [] for i in range(self.model().rowCount()): if self.model().item(i).checkState() == Qt.Checked: texts.append(self.model().item(i).text()) text = ", ".join(texts) # Compute elided text (with "...") metrics = QFontMetrics(self.lineEdit().font()) elidedText = metrics.elidedText(text, Qt.ElideRight, self.lineEdit().width()) self.lineEdit().setText(elidedText) def addItem(self, text, data=None): item = QStandardItem() item.setText(text) if data is None: item.setData(text) else: item.setData(data) item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsUserCheckable) item.setData(Qt.Unchecked, Qt.CheckStateRole) self.model().appendRow(item) def addItems(self, texts, datalist=None): for i, text in enumerate(texts): try: data = datalist[i] except (TypeError, IndexError): data = None self.addItem(text, data) def currentData(self): # Return the list of selected items data res = [] for i in range(self.model().rowCount()): if self.model().item(i).checkState() == Qt.Checked: res.append(self.model().item(i).data()) return res def currentIndexes(self): # Return the list of selected items id res = [] for i in range(self.model().rowCount()): if self.model().item(i).checkState() == Qt.Checked: res.append(i) return res # class CheckableComboBox(QComboBox): # def __init__(self): # super().__init__() # self._changed = False # self.view().pressed.connect(self.handleItemPressed) # # def setItemChecked(self, index, checked=False): # item = self.model().item(index, self.modelColumn()) # QStandardItem object # if checked: # item.setCheckState(Qt.Checked) # else: # item.setCheckState(Qt.Unchecked) # # def handleItemPressed(self, index): # item = self.model().itemFromIndex(index) # if item.checkState() == Qt.Checked: # item.setCheckState(Qt.Unchecked) # else: # item.setCheckState(Qt.Checked) # self._changed = True # # def hidePopup(self): # if not self._changed: # super().hidePopup() # self._changed = False # # def itemChecked(self, index): # item = self.model().item(index, self.modelColumn()) # return item.checkState() == Qt.Checked