6

I have a very long list of values and I would like to make things easier for the user by allowing to filter the items of the QgsCheckableCombobox with a lineEdit for instance.

With this code I have managed to filter the items of the combo-box but I can't check or uncheck the filtered items...

values_list = []

for i in layer.getFeatures():
    values_list.append(str(i.attributes()[2]))

completer = QCompleter(values_list)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
self.lineEdit.setCompleter(completer)

self.mComboBox.setLineEdit(self.lineEdit)
self.mComboBox.addItems(values_list)

2 Answers 2

6

By replacing the line edit on the combobox you are probably interfering with the custom model of the QgsCheckableComboBox. Therefore the checkbox does not work anymore.

Instead what you can do is filter the items on each keystroke by connecting to the textChanged signal of your line edit.

class MyWindow(QDialog):

    def __init__(self, parent):
        super().__init__(parent)
        
        layout = QVBoxLayout()
        self.mComboBox = QgsCheckableComboBox()
        self.lineEdit = QLineEdit(self)
        
        self.lineEdit.textChanged.connect(self.onTextChanged)
        
        self.values_list = ['Berlin', 'Madrid', 'Rome', 'Bucharest']

        self.mComboBox.addItems(self.values_list)
        
        layout.addWidget(self.lineEdit)
        layout.addWidget(self.mComboBox)
        self.setLayout(layout)

    def onTextChanged(self, filter_text):
        filtered_items = [
            item for item in self.values_list if item.lower().startswith(filter_text.lower())
        ]
        
        checked_items = self.mComboBox.checkedItems()
        
        self.mComboBox.clear()
        self.mComboBox.addItems(filtered_items)
        
        # retain checked items 
        for checked_item in checked_items:
            index = self.mComboBox.findText(checked_item)
            if index > -1:
                self.mComboBox.setItemCheckState(index, Qt.Checked)
                
dlg = MyWindow(iface.mainWindow())
dlg.show()

Screencast demo of the code above:

screencast

5

Another approach, as an alternative to using a QgsCheckableComboBox, is to create a custom widget wrapper and implement a QListWidget with checkable items, and two QLineEdit(s), one to filter the list widget, and one to hold the checked items.

Although it's more involved, I like this approach because you get to see the active filtering in real time as you type and delete characters in the filter line edit, instead of somewhat blindly typing the filter string then clicking the drop down arrow on the combo box to see the result.

The general behavior of this widget should very closely match that of a QgsCheckableComboBox, including the context menu when you right-click inside the list widget viewport, allowing the user to select or deselect all items.

The selected items can be retrieved via the selected_items() method which will return a list of strings.

You can copy the CustomCheckableListWidget class from the code block below and use it in your PyQGIS project/ plugin etc. I have provided an example of using it in a very basic QDialog.

class TestDialog(QDialog):
    
    def __init__(self, iface):
        self.iface = iface
        QDialog.__init__(self)
        self.setGeometry(500, 300, 500, 300)
        # Create an instance of the widget wrapper class
        self.list_widget = CustomCheckableListWidget(self)
        # Call set_items() method and pass your list of feature attributes
        self.list_widget.set_items([str(f.attributes()[2]) for f in iface.activeLayer().getFeatures()])
        self.layout = QVBoxLayout(self)
        self.layout.addWidget(self.list_widget)
        

class CustomCheckableListWidget(QWidget):
    '''
    Copy and paste this class into your PyQGIS project/ plugin
    '''
    
    def __init__(self, parent=None):
        self.parent = parent
        QDialog.__init__(self)
        self.layout = QVBoxLayout(self)
        self.filter_le = QLineEdit(self)
        self.filter_le.setPlaceholderText('Type to filter...')
        self.items_le = QLineEdit(self)
        self.items_le.setReadOnly(True)
        self.lw = QListWidget(self)
        self.lw.setMinimumHeight(100)
        self.layout.addWidget(self.filter_le)
        self.layout.addWidget(self.items_le)
        self.layout.addWidget(self.lw)
        
        self.lw.viewport().installEventFilter(self)
        
        self.filter_le.textChanged.connect(self.filter_items)
        self.context_menu = QMenu(self)
        self.action_check_all = QAction('Select All', self)
        self.action_check_all.triggered.connect(self.select_all)
        self.action_uncheck_all = QAction('De-select All', self)
        self.action_uncheck_all.triggered.connect(self.deselect_all)
        self.context_menu.addAction(self.action_check_all)
        self.context_menu.addAction(self.action_uncheck_all)
                
    def select_all(self):
        for i in range(self.lw.count()):
            item = self.lw.item(i)
            if not item.isHidden():
                item.setCheckState(Qt.Checked)
        self.update_items()

    def deselect_all(self):
        for i in range(self.lw.count()):
            item = self.lw.item(i)
            if not item.isHidden():
                item.setCheckState(Qt.Unchecked)
        self.update_items()

    def set_items(self, item_list):
        self.lw.clear()
        for i in item_list:
            lwi = QListWidgetItem(i)
            lwi.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
            lwi.setCheckState(Qt.Unchecked)
            self.lw.addItem(lwi)
            
    def eventFilter(self, obj, event):
        if event.type() == QEvent.MouseButtonPress and obj == self.lw.viewport():
            if event.button() == Qt.LeftButton:
                clicked_item = self.lw.itemAt(event.pos())
                if clicked_item.checkState() == Qt.Checked:
                    clicked_item.setCheckState(Qt.Unchecked)
                else:
                    clicked_item.setCheckState(Qt.Checked)
                self.update_items()
            elif event.button() == Qt.RightButton:
                self.context_menu.exec(QCursor.pos())
            return True
        return False
            
    def filter_items(self, filter_txt):
        for i in range(self.lw.count()):
            item = self.lw.item(i)
            filter = filter_txt.lower() not in item.text().lower()
            self.lw.setRowHidden(i, filter)
    
    def update_items(self):
        self.items_le.clear()
        selection = []
        for i in range(self.lw.count()):
            item = self.lw.item(i)
            if item.checkState() == Qt.Checked:
                selection.append(item.text())
        selection.sort()
        self.items_le.setText(', '.join(selection))
        
    def checked_items(self):
        selection = []
        for i in range(self.lw.count()):
            item = self.lw.item(i)
            if item.checkState() == Qt.Checked:
                selection.append(item.text())
        selection.sort()
        return selection
        
dlg = TestDialog(iface)
dlg.show()

Demo screencast:

enter image description here

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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