Qt Custom Multi-Select QComboBox from SQL Model

1.1k Views Asked by At

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! :)

1

There are 1 best solutions below

1
On BEST ANSWER

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_())