0

I'm using Pyside2, Python 3.8. I have a QTableView, the value of the first column is a bool, I want to implement a delegate to paint Editable CheckBoxs in the first column. I've tried many SO answers, the one that've been the most helpful is this one. It partially works, ie: You have to double click the cell, then you get a 'kind of' combobox instead of a checkbox in my first column (see picture below)

enter image description here

I've managed to do this without going through a Delegate, using the CkeckStateRole, it worked, the checkboxs aren't editable, but I have many editable other columns and It seems that there's no escape from implementing a delegate as it is the more proper way to do it.

here's my Table Model class:

class TableModel(QtCore.QAbstractTableModel):

def __init__(self, mlist=None):
    super(TableModel, self).__init__()
    self._items = [] if mlist == None else mlist
    self._header = []

def rowCount(self, parent = QtCore.QModelIndex):
    return len(self._items)

def columnCount(self, parent = QtCore.QModelIndex):
    return len(self._header)

def flags(self, index):
    if index.column() == 0:
        return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled
    return QtCore.Qt.ItemIsEnabled

def data(self, index, role = QtCore.Qt.DisplayRole):
    if not index.isValid():
       return None
    elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
        return self._items[index.row()][index.column()]
    elif role == QtCore.Qt.CheckStateRole:
        return None
    else:
        return None

def setData(self, index, value, role = QtCore.Qt.EditRole):
    if value is not None and role == QtCore.Qt.EditRole:
        self._items[index.row()][index.column()] = value
        self.dataChanged.emit(index, index)
        return True
    return False

Here's my CheckBox Delegate class:

class CheckBoxDelegate(QtWidgets.QItemDelegate):

def __init__(self, parent = None):
    QtWidgets.QItemDelegate.__init__(self, parent)

def createEditor(self, parent, option, index):
    if not (QtCore.Qt.ItemIsEditable & index.flags()): 
        return None
    check = QtWidgets.QCheckBox(parent)
    check.clicked.connect(self.stateChanged)

    return check

def setEditorData(self, editor, index):
    editor.blockSignals(True)
    editor.setChecked(index.data())
    editor.blockSignals(False)

def setModelData(self, editor, model, index):
    model.setData(index, editor.isChecked(), QtCore.Qt.EditRole)

def paint(self, painter, option, index):
    value = index.data()
    if value:
        value = QtCore.Qt.Checked
    else:
        value = QtCore.Qt.Unchecked

    self.drawCheck(painter, option, option.rect, value)
    self.drawFocus(painter, option, option.rect)

@QtCore.Slot()
def stateChanged(self):
    print("sender", self.sender())
    self.commitData.emit(self.sender())

Edit #1

After some research, I've found out that I've forgot to put self.MyTableView.setItemDelegateForColumn(0, CheckBoxDelegate(self)) in my MainWindow __init__

Well, What happens now is I get a centered CheckBox in my first column, I can't edit it, but, when I double click it, It creates a second Checkbox in the same cell at the left, which is Editable and also sets the new CheckState to my Model. Let's say my centered CheckBox is set to True, I double click, a second Checked CheckBox is created to its left, I Uncheck it, I click on another cell, the second checkBox disappears, the centered checkBox is set to Unchecked, and my ModelData is updated. (see picture below for what happens after double clicking the centered CheckBox)

enter image description here

Fix attempts

1. Not overriding paint():

The column contains either True or false, double clicking the cell creates an editable checkbox which sets the data in the model to the new value and then disappears.

2. the createEditor() method returns None

def createEditor(self, parent, option, index):
      return None

It creates a single centered checkbox, no labels, but the checkbox isn't editable

3. Not overriding createEditor() method

Creates a single centered checkbox with no labels, double clicking the cell replace the checkbox with a combobox (True or False), from which I can change the CheckBox state.

Neither of those fixes gave me what I'm looking for: A signe centered checkbox created that is editable with a single click

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Hamouza
  • 356
  • 3
  • 12
  • I'm not sure I'm understanding your issue. You just want checkboxes in the first column, and allow the user to check or uncheck them? – musicamante Jan 08 '21 at 12:30
  • Yes Indeed ! the check/uncheck action should update the data in the Table Model, It'd be great If I can do this through a Delegate as I intend to use it for other tables, and edit other columns. – Hamouza Jan 08 '21 at 12:42
  • Ehm, you don't need to use any delegate at all, you just need to correctly add the `ItemIsUserCheckable` flag in `flags()`. – musicamante Jan 08 '21 at 12:49

1 Answers1

1

There is absolutely no need for a custom delegate, as it's already supported by the default one. You just need to correctly return the ItemIsUserCheckable flag, and implement the CheckStateRole in both data() and setData().

class TableModel(QtCore.QAbstractTableModel):
    def __init__(self, mlist=None, checkableColumns=None):
        super(TableModel, self).__init__()
        self._items = [] if mlist == None else mlist
        self._header = ['aaaa', 'bbb', 'ccc']
        if checkableColumns is None:
            checkableColumns = []
        elif isinstance(checkableColumns, int):
            checkableColumns = [checkableColumns]
        self.checkableColumns = set(checkableColumns)

    def setColumnCheckable(self, column, checkable=True):
        if checkable:
            self.checkableColumns.add(column)
        else:
            self.checkableColumns.discard(column)
        self.dataChanged.emit(
            self.index(0, column), self.index(self.rowCount() - 1, column))

    def rowCount(self, parent = QtCore.QModelIndex):
        return len(self._items)

    def columnCount(self, parent = QtCore.QModelIndex):
        return len(self._header)

    def flags(self, index):
        flags = QtCore.Qt.ItemIsEnabled
        if index.column() in self.checkableColumns:
            # only this flag is required
            flags |= QtCore.Qt.ItemIsUserCheckable
        return flags

    def data(self, index, role = QtCore.Qt.DisplayRole):
        if not index.isValid():
           return None
        if (role == QtCore.Qt.CheckStateRole and 
            index.column() in self.checkableColumns):
                value = self._items[index.row()][index.column()]
                return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
        elif (index.column() not in self.checkableColumns and 
            role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole)):
                return self._items[index.row()][index.column()]
        else:
            return None

    def setData(self, index, value, role = QtCore.Qt.EditRole):
        if (role == QtCore.Qt.CheckStateRole and 
            index.column() in self.checkableColumns):
                self._items[index.row()][index.column()] = bool(value)
                self.dataChanged.emit(index, index)
                return True
        if value is not None and role == QtCore.Qt.EditRole:
            self._items[index.row()][index.column()] = value
            self.dataChanged.emit(index, index)
            return True
        return False

Then, if you want to center the checkbox whenever the index has no text, you can simply use a proxy style:

class ProxyStyle(QtWidgets.QProxyStyle):
    def subElementRect(self, element, opt, widget=None):
        if element == self.SE_ItemViewItemCheckIndicator and not opt.text:
            rect = super().subElementRect(element, opt, widget)
            rect.moveCenter(opt.rect.center())
            return rect
        return super().subElementRect(element, opt, widget)

# ...

app.setStyle(ProxyStyle())
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for your comment. It brings me back to why I wanted, or rather figured out after tons of research that I need to do it using a delegate. first, I have several TableViews In my app, each one use a different TableModel model, Not every one of them have checkboxs in the first column. second, I want to be able to change the value of the bool just by a single click on the checkbox, using this method, I should double click the cell and then change the bool value through a "combobox". – Hamouza Jan 08 '21 at 13:35
  • @Hamouza Using the code above you *don't* need to double click, as the checkable items in views require a single click; ensure that you're correctly using my code. Then, if you have different models and each model has a different column for the checkboxes, just set the checkable columns in the model with a custom function and check for that column in the `flags()`, `data()` and `setData()` (see the update). – musicamante Jan 08 '21 at 14:00
  • Hey ! Thanks !! It works, I have a single checkbox that is checkable as I wanted to. I'm encountring different problem now is that my LessThan() method in my proxymodel doesn't work, as a & b used to be a bools, now the method get a 'NoneType' object. Also, which is kind of weird, in my other tables, the first column is empty – Hamouza Jan 08 '21 at 15:54
  • @Hamouza about the last issue, that's my bad: remove the `index.column()` in the `elif` of `data()`. For the proxy model, that depends. The problem is that a proxy model uses the `DisplayRole`, but you need that to be empty in order to display the checkbox only. You could use [`setSortRole()`](https://doc.qt.io/qt-5/qsortfilterproxymodel.html#sortRole-prop) to `CheckStateRole` if the column is a checkable one, or create your own custom role and always set that, then in the model check if the requested role is your custom one, and in that case just return the actual data. – musicamante Jan 08 '21 at 16:05
  • I removed the index.column() and it works perfectly fine now, so does the sorting so no need for an additional fixing, Do you know how do I get rid of the label next to the checkbox please ? if it's unchecked the label is 0, else, it's 2 – Hamouza Jan 08 '21 at 16:14
  • The removal of index.column() was wrong, sorry again, it should be `elif index.column() not in self.checkableColumns and role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):`. This will obviously recreate the sorting issue, which you should address as suggested before. – musicamante Jan 08 '21 at 16:21
  • Is doing so will make the label to not be displayed anymore ? if not, I'd rather just leave it like it is as I honestly will have to dig more into this to get the sorting right, and it's been already several days that I'm stuck on setting the checkboxs in the column – Hamouza Jan 08 '21 at 16:25
  • 1
    @Hamouza Don't ask what it does, try it. I updated the code accordingly. – musicamante Jan 08 '21 at 16:39
  • I've updated the data() method and also changed the role in Lessthan() method to CheckStateRole when index.column() == 0, this works perfectly fine now ! I'm still trying to figure out a way to hide the label next to the checkbox, no success – Hamouza Jan 10 '21 at 16:11
  • @Hamouza Check your modifications, then, because with the code in my answer there is absolutely *no* label next to the checkbox. And remember that you don't need a delegate for this. – musicamante Jan 10 '21 at 16:16
  • I've been following your comments and gave up the delegate method, Indeed, I'm now creating Checkboxs and changinng their value just using the data() and setData() methods in my Model, but I still have the label displayed. I'm going to dig further into this, and create a new SO post with the updated code. – Hamouza Jan 10 '21 at 16:19
  • 1
    @Hamouza before creating a new question, I suggest you to create a new script with my code (in the last update I included all the required methods) and do some testing with it alone, then check the differences to understand why your code shows the display text and mine doesn't. Ensure that you're using *exactly* my code. – musicamante Jan 10 '21 at 17:10
  • I've created a new script, with a more minimalistic view and data to see why it works but not in my case, I put the two script side to side and spotted the bug. Thanks a lot man !! – Hamouza Jan 10 '21 at 20:17
  • I've been trying to understand the ProxyStyle part, it returns an error in if element == self.SE_ItemViewItemCheckIndicator and not opt.text: AttributeError: 'PySide2.QtWidgets.QStyleOption' object has no attribute 'text' – Hamouza Jan 10 '21 at 20:18
  • 1
    Try adding `itemOpt = QtWidgets.QStyleOptionViewItem(opt)` at the beginning of that function (before the `if element ==`) and change the `opt` references to `itemOpt` (except for the `super()` part). – musicamante Jan 10 '21 at 20:25