Search code examples
pythonpyqtpyqt5qcomboboxqsqlquerymodel

Qt Custom Multi-Select QComboBox from SQL Model


I want a drop-down category selection box which allows the user to click multiple selections, and which preferably is connected to an SQL query. I'm not attached to the Combobox in particular (disappearing on each click isn't helpful), whatever can get the job done.

At the current moment, I have basically that jerry-rigged together, it allows for multiple selections technically but it's just based on randomly if the mouse is dragged over it or not.

self.catbx=QComboBox()
...
self.catq=QtSql.QSqlQuery(conn)
self.catq.exec("SELECT name FROM categories")
self.catmo=QtSql.QSqlQueryModel()
self.catmo.setQuery(self.catq)
self.catbx.setModel(self.catmo)
...
self.catview=QListView()
self.catview.setModel(self.catmo)
self.catbx.setView(self.catview)
self.catview.setSelectionMode(QAbstractItemView.MultiSelection)

hope that's clear enough and someone can help! :)


Solution

  • Basically it is that the items are not selectable, since that is the event that triggers the closing of the popup so the solution is to eliminate that flag in the model as I did in a previous answer.

    On the other hand the option to be checkeable does not come by default in the QSqlQueryModel, so we have to implement it for that we based on another previous answer.

    Finally, a class is created that inherits from QComboBox and we overwrite the hidePopup() method to emit a signal that sends the selected items.

    Update:

    If you want to also be marked when you press any part of the item you must create a delegate and overwrite the editorEvent() method so that it handles the MouseButtonRelease event. But this brings a small problem: to open the popup you have to press the item that is displayed so it will open marked.

    from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
    
    class CheckSqlQueryModel(QtSql.QSqlQueryModel):
        def __init__(self, *args, **kwargs):
            QtSql.QSqlQueryModel.__init__(self, *args, **kwargs)
            self.checks = {}
    
        def checkState(self, pindex):
            if pindex not in self.checks.keys():
                self.checks[pindex] = QtCore.Qt.Unchecked
            return self.checks[pindex]
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if role == QtCore.Qt.CheckStateRole and index.isValid():
                return self.checkState(QtCore.QPersistentModelIndex(index))
            return QtSql.QSqlQueryModel.data(self, index, role)
    
        def setData(self, index, value, role=QtCore.Qt.EditRole):
            if role == QtCore.Qt.CheckStateRole and index.isValid():
                self.checks[QtCore.QPersistentModelIndex(index)] = value
                return True
            return QtSql.QSqlQueryModel(self, index, value, role)
    
        def flags(self, index):
            fl = QtSql.QSqlQueryModel.flags(self, index) & ~QtCore.Qt.ItemIsSelectable 
            fl |= QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
            return fl
    
    class CheckComboBox(QtWidgets.QComboBox):
        selectedChanged = QtCore.pyqtSignal(list)
    
        def hidePopup(self):
            results = []
            for i in range(self.count()):
                if self.itemData(i, QtCore.Qt.CheckStateRole) == QtCore.Qt.Checked:
                    results.append(self.itemText(i))
            self.selectedChanged.emit(results)
            QtWidgets.QComboBox.hidePopup(self)
    
    class CheckDelegate(QtWidgets.QStyledItemDelegate):
        def editorEvent(self, event, model, option, index):
            if event.type() == QtCore.QEvent.MouseButtonRelease:
                val = index.data(QtCore.Qt.CheckStateRole)
                new_val = QtCore.Qt.Checked if val == QtCore.Qt.Unchecked else QtCore.Qt.Unchecked
                model.setData(index, new_val, QtCore.Qt.CheckStateRole)
                return True
            return QtWidgets.QStyledItemDelegate.editorEvent(self, event, model, option, index)
    
    
    class Widget(QtWidgets.QWidget):
        def __init__(self, *args, **kwargs):
            QtWidgets.QWidget.__init__(self, *args, **kwargs)
            lay = QtWidgets.QVBoxLayout(self)
    
            combo = CheckComboBox()
            combo.setView(QtWidgets.QListView())
            combo.setItemDelegate(CheckDelegate(combo))
            model = CheckSqlQueryModel()
            model.setQuery("SELECT name FROM categories")
            combo.setModel(model)
    
            self.lw = QtWidgets.QListWidget()
            combo.selectedChanged.connect(self.on_selectedChanged)
    
            lay.addWidget(combo)
            lay.addWidget(self.lw)
    
        def on_selectedChanged(self, items):
            self.lw.clear()
            self.lw.addItems(items)
    
    def createConnection():
        db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(":memory:")
        if not db.open():
            QtWidgets.QMessageBox.critical(None, "Cannot open database",
                                 "Unable to establish a database connection.\n"
                                 "This example needs SQLite support. Please read "
                                 "the Qt SQL driver documentation for information how "
                                 "to build it.\n\n"
                                 "Click Cancel to exit.", QMessageBox.Cancel)
            return False
        query = QtSql.QSqlQuery()
        query.exec_("create table categories (id int primary key, name varchar(20))");
        for i in range(1, 10):
             query.exec_("insert into categories values({i}, 'categories-{i}')".format(i=i));
    
        return True
    
    if __name__ == "__main__":
        import sys
        app = QtWidgets.QApplication(sys.argv)
        if not createConnection():
            sys.exit(-1)
        w = Widget()
        w.show()
        sys.exit(app.exec_())