2

I'm making the gui for my QGIS3 plugin using PyQT5 and QT designer. I have a very long list of items in a QComboBox and I need the user to be able to select one or more items. I know that QComboBox allows only single selection, so I thought that I can use QListWidget. The problem is that the list of selectable items is very long and I'd need a dropdown menu or something similar.

Is there a way to style a QListWidget like a QComboBox or to allow a multiple selection using QComboBox?

EDIT: I added an immage of my GUI. This is a long list and, as you can see, it's the reason why I can't use QListWidget or QTableWidget. A dropdown checkbox group should be perfect and I'm surprised that QT doesn't make this available.

enter image description here

  • I think that the list widget is the component thought for your problem (multiple selection). Remember that you can give it a reasonable small height and the widget will scroll. – cabesuon Feb 10 at 17:05
  • Hi, thanks for your answer. It would be useful for the user to see all the elements simultaneously, this is the principal reason why I asked for a dropdown. – Lorenzo Feb 10 at 17:10
3

For anyone interested, here is an implementation of a CheckableComboBox.

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

Usage

comunes = ['Ameglia', 'Arcola', 'Bagnone', 'Bolano', 'Carrara', 'Casola', 'Castelnuovo Magra', 
    'Comano, località Crespiano', 'Fivizzano', 'Fivizzano località Pieve S. Paolo', 
    'Fivizzano località Pieve di Viano', 'Fivizzano località Soliera', 'Fosdinovo', 'Genova', 
    'La Spezia', 'Levanto', 'Licciana Nardi', 'Lucca', 'Lusuolo', 'Massa', 'Minucciano', 
    'Montignoso', 'Ortonovo', 'Piazza al sercho', 'Pietrasanta', 'Pignine', 'Pisa',
    'Podenzana', 'Pontremoli', 'Portovenere', 'Santo Stefano di Magra', 'Sarzana',
    'Serravezza', 'Sesta Godano', 'Varese Ligure', 'Vezzano Ligure', 'Zignago' ]
combo = CheckableComboBox()
combo.addItems(comunes)

Result

Checkable ComboBox


Edit

QGIS defines its own QgsCheckableCombobox, which is unfortunately broken (https://github.com/qgis/QGIS/issues/28658).

It relies on the QCombBox activated signal to toggle on/off its items. The problem is that this signal is emitted when the combobox is opened and there's a single selected item, which is NOT the currentIndex

It's however possible to fix it, albeit in a slightly hackish way

class CheckableComboBox2(QgsCheckableComboBox):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.activated.connect(self.afterActivated)   

    def afterActivated(self):
        for i in range ( self.model().rowCount()):
            item = self.model().item(i)  
            if item.checkState() == Qt.Checked:
                self.setCurrentIndex(i)
                self.model().itemCheckStateChanged.emit()
                return
| improve this answer | |
  • Thanks for sharing. Do you know qgis.org/api/classQgsCheckableComboBox.html ? There is one issue, maybe you could spot it in the code? github.com/qgis/QGIS/issues/28658 – etrimaille Feb 18 at 20:16
  • No I was not aware of this Class. From what I understand, it relies on the QCombBox activated signal to toggle on/off its items. The problem is that this signal is emitted when the combobox is opened and there's a single selected item, which is NOT the currentIndex. A solution would be to set the currentIndex to the first selected item in QgsCheckableComboBox::setItemCheckState when an item is toggled off. – Yoann Quenach de Quivillic Feb 19 at 9:44
  • ok thanks for looking. Feel free to do a PR if you can. Can you add your answer on the Github ticket too. Thanks – etrimaille Feb 19 at 10:25
  • Sorry, I just noticed you made an answer there. – etrimaille Feb 19 at 10:27
  • This is what I was looking for. Thanks again – Lorenzo Feb 19 at 18:02
2

You're right. QComboBox objects allow only single selection because they don't have ExtendedSelection option (as QListWidget objects) for activating multiple selection. However, you can also use a QTableWidget object whose ExtendedSelection option is already activated by default. In following code you have an example.

from PyQt5.QtCore import Qt

class Dlg(QDialog):

    def __init__(self):
        QDialog.__init__(self)
        self.layout = QGridLayout(self)

        self.tabs = QTabWidget()
        self.tab1 = QWidget()
        self.tab2 = QWidget()
        self.tab3 = QWidget()

        self.tabs.addTab(self.tab1,"Tab1")
        self.tabs.addTab(self.tab2,"Tab2")
        self.tabs.addTab(self.tab3,"Tab3")

        layer = iface.activeLayer()

        nf = layer.fields().names()

        feats = [ feat for feat in layer.getFeatures() ]

        data = [ [] for i in range(len(feats)) ]

        nb_row = len(feats)
        nb_col = len(nf)

        for i, feat in enumerate(feats):
            for j in range(nb_col):
                data[i].append(feat.attribute(nf[j]))

        self.tab1.layout = QVBoxLayout(self)

        self.table1 = QTableWidget()
        self.table1.setRowCount(nb_row)
        self.table1.setColumnCount(nb_col)
        self.table1.setHorizontalHeaderLabels(nf)

        for row in range (nb_row):
            for col in range(nb_col):
                item = QTableWidgetItem(str(data[row][col]))
                self.table1.setItem(row, col, item)

        self.tab1.layout.addWidget(self.table1)
        self.tab1.setLayout(self.tab1.layout)

        self.layout.addWidget(self.tabs, 0, 0)

w = Dlg()
w.resize(350,300)
w.setWindowTitle('Example with QTableWidget')
w.setWindowFlags(Qt.WindowStaysOnTopHint)
w.show()

I used it for printed in this kind of object (QTableWidget) the values of each field in attributes Table of active layer; as it can be observed in following image.

enter image description here

In following images it can be observed multiple selection by rows:

enter image description here

or by columns:

enter image description here

Editing Note:

Why can you not use a QTableWidget? I modified above code for including your long list and some labels for creating a similar context with your form. Code looks as follows:

from PyQt5.QtCore import Qt

class Dlg(QDialog):

    def __init__(self):
        QDialog.__init__(self)
        self.layout = QGridLayout(self)
        self.label1 = QLabel('Visualizzare tutti i beni per:')
        self.label2 = QLabel('Macro-epoca originaria:')

        comunes = ['Ameglia', 'Arcola', 'Bagnone', 'Bolano', 'Carrara', 'Casola', 'Castelnuovo Magra',
                   'Comano, località Crespiano', 'Fivizzano', 'Fivizzano località Pieve S. Paolo', 
                   'Fivizzano località Pieve di Viano', 'Fivizzano località Soliera', 'Fosdinovo'
                   'Genova', 'La Spezia', 'Levanto', 'Licciana Nardi', 'Lucca', 'Lusuolo', 'Massa',
                   'Minucciano', 'Montignoso', 'Ortonovo', 'Piazza al sercho', 'Pietrasanta', 'Pignine',
                   'Pisa', 'Podenzana', 'Pontremoli', 'Portovenere', 'Santo Stefano di Magra', 'Sarzana',
                   'Serravezza', 'Sesta Godano', 'Varese Ligure', 'Vezzano Ligure', 'Zignago' ]

        nb_row = len(comunes)
        nb_col = 1

        data = [ [] for i in range(nb_row) ]

        for i, comune in enumerate(comunes):
            for j in range(nb_col):
                data[i].append(comune)

        self.table1 = QTableWidget()
        self.table1.horizontalHeader().setStretchLastSection(True)

        self.table1.setRowCount(nb_row)
        self.table1.setColumnCount(nb_col)
        self.table1.setHorizontalHeaderLabels(['Comune'])

        for row in range (nb_row):
            for col in range(nb_col):
                item = QTableWidgetItem(str(data[row][col]))
                self.table1.setItem(row, col, item)

        self.layout.addWidget(self.label1, 0, 0)
        self.layout.addWidget(self.table1, 1, 0)
        self.layout.addWidget(self.label2, 2, 0)

w = Dlg()
w.resize(350,300)
w.setWindowTitle('Ricerca beni')
w.setWindowFlags(Qt.WindowStaysOnTopHint)
w.show()

After running it, you can observe in following image that it is possible to scroll all 36 items and to realize multiple selection with them. You can also use the hide mode with the QTableWidget for visualizing options by using a check button (yor desired dropdown checkbox group).

enter image description here

| improve this answer | |
  • Hi, thanks for your answer. I edited my question to make it more clear. – Lorenzo Feb 11 at 16:13
  • Please, see my editing note. – xunilk Feb 12 at 4:08
  • Hi many thanks again. My ideas now are quite more clear and I'll probably use your solution. I had not thought to hide it. In this way I could keep reduced the dialog's size. – Lorenzo Feb 12 at 16:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.