0

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

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
user26472
  • 97
  • 1
  • 10
  • Do you want a combobox with checkboxes? – Dmitry Sazonov May 18 '18 at 08:24
  • The checkboxes list with a button which controls the list show/hide under the button?! – JustWe May 18 '18 at 08:42
  • @DmitrySazonov that would be ok, as long as it didn't disappear on every click! Or even just a combobox whose items are checkable – user26472 May 19 '18 at 04:58
  • @user26472 what event do you want the popup to close? – eyllanesc May 19 '18 at 06:02
  • @eyllanesc not entirely sure what ur asking, but what i'm trying to have is a popup "category select" menu which will be attached to a filter of my app's main table. There are a decent number of categories to choose (multiple selections) from, so I want the drop-down functionality of qcombobox with a window which only disappears when the user clicks outside of it, upon which it will filter the data. Is that clear? – user26472 May 19 '18 at 06:10
  • @user26472 Yes, the answer to my question was to close when pressed out of the popup. – eyllanesc May 19 '18 at 06:12

1 Answers1

2

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you!! Is there any way to get a click on the label name to also count for the checkbox? i tried messing around with the flags and stuff but no luck. Other than that though it's functional now, thanks :) – user26472 May 20 '18 at 03:54