alex_bits
alex_bits

Reputation: 742

No background for selected cell in QItemDelegate modified cell

I am using a subclassed QItemDelegate to display and edit cells in a QTableView. The editing part works perfectly.
The issue I have is that for every cell that is editable, the background stays white when the row is selected (I use a row selection behavior).
I have tried playing with the paint() method but whatever I do the background for those cells does not change when the row is selected.
My internet research leads me to think that because the different widgets used to edit the cells take up the entire cell rect (since that is how it is defined in the updateEditorGeometry() method), the background of the cell if hidden.
How can i fix this so that the background of the widget used to edit the cell gets the same selected background as the rest of the row.
Here is an image displaying the issue:

enter image description here

Each of the cells that still has a white background is editable and is defined in the QItemDelegate class.
Here is the code for the delegate class:

class SqlAlchemyItemDelegate(QtWidgets.QItemDelegate):
    def __init__(self):
        super().__init__()

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:

        # Checkboxes
        check_state = index.data(QtCore.Qt.CheckStateRole)
        if isinstance(check_state, int):
            self.drawCheck(painter, option, option.rect, check_state)
            self.drawFocus(painter, option, option.rect)
            return
        super().paint(painter, option, index)

    def editorEvent(self, event, model, option, index):
        if event.type() == QtCore.QEvent.MouseButtonRelease and isinstance(index.data(QtCore.Qt.DisplayRole), bool):
            value = bool(model.data(index, QtCore.Qt.CheckStateRole))
            model.setData(index, not value)
            event.accept()
        return super().editorEvent(event, model, option, index)

    def createEditor(self, parent: QtWidgets.QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtWidgets.QWidget:
        data = index.model().data(index, QtCore.Qt.EditRole)
        if isinstance(data, datetime.datetime):
            return QtWidgets.QDateTimeEdit(parent)
        if isinstance(data, str):
            return QtWidgets.QLineEdit(parent)

    def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex) -> None:
        data = index.model().data(index, QtCore.Qt.EditRole)
        if isinstance(editor, QtWidgets.QDateTimeEdit):
            editor.setDateTime(data)
        if isinstance(editor, QtWidgets.QLineEdit):
            editor.setText(data)

    def setModelData(self, editor: QtWidgets.QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex) -> None:
        if isinstance(editor, QtWidgets.QDateTimeEdit):
            model.setData(index, editor.dateTime().toPyDateTime(), QtCore.Qt.EditRole)
        if isinstance(editor, QtWidgets.QLineEdit):
            model.setData(index, editor.text(), QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor: QtWidgets.QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
        editor.setGeometry(option.rect)

EDIT
Here is a minimal working example that reproduces the issue:

from PyQt5 import QtWidgets, QtCore, QtGui
import datetime


class Gui(QtWidgets.QDialog):
    def __init__(self):
        super().__init__()
        self.setMinimumWidth(750)

        self.table_view = QtWidgets.QTableView()
        self.table_view.setSelectionBehavior(self.table_view.SelectRows)
        self.table_model = SqlAlchemyTableModel()
        self.table_model.from_complex_query(
            table_data=[
                [1, 'Alex', 'Knight', '28', '0123456789', True, datetime.datetime(year=2021, month=5, day=12, hour=2, minute=30, second=30)],
                [2, 'Alex', 'Knight', '28', '0123456789', True, datetime.datetime(year=2021, month=5, day=12, hour=2, minute=30, second=30)],
                [3, 'Alex', 'Knight', '28', '0123456789', True, datetime.datetime(year=2021, month=5, day=12, hour=2, minute=30, second=30)]
            ],
            headers=['id', 'first', 'last', 'age', 'phone', 'checked', 'date'],
            editable_columns=[4, 5, 6]
        )
        self.table_view.setModel(self.table_model)
        self.delegate = SqlAlchemyItemDelegate()
        self.table_view.setItemDelegate(self.delegate)

        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.addWidget(self.table_view)
        self.setLayout(self.layout)
        self.show()


class SqlAlchemyItemDelegate(QtWidgets.QItemDelegate):
    def __init__(self):
        super().__init__()

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:

        # Checkboxes
        check_state = index.data(QtCore.Qt.CheckStateRole)
        if isinstance(check_state, int):
            self.drawCheck(painter, option, option.rect, check_state)
            self.drawFocus(painter, option, option.rect)
            return
        super().paint(painter, option, index)

    def editorEvent(self, event, model, option, index):
        if event.type() == QtCore.QEvent.MouseButtonRelease and isinstance(index.data(QtCore.Qt.DisplayRole), bool):
            value = bool(model.data(index, QtCore.Qt.CheckStateRole))
            model.setData(index, not value)
            event.accept()
        return super().editorEvent(event, model, option, index)

    def createEditor(self, parent: QtWidgets.QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> QtWidgets.QWidget:
        data = index.model().data(index, QtCore.Qt.EditRole)
        if isinstance(data, datetime.datetime):
            return QtWidgets.QDateTimeEdit(parent)
        if isinstance(data, str):
            return QtWidgets.QLineEdit(parent)

    def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex) -> None:
        data = index.model().data(index, QtCore.Qt.EditRole)
        if isinstance(editor, QtWidgets.QDateTimeEdit):
            editor.setDateTime(data)
        if isinstance(editor, QtWidgets.QLineEdit):
            editor.setText(data)

    def setModelData(self, editor: QtWidgets.QWidget, model: QtCore.QAbstractItemModel, index: QtCore.QModelIndex) -> None:
        if isinstance(editor, QtWidgets.QDateTimeEdit):
            model.setData(index, editor.dateTime().toPyDateTime(), QtCore.Qt.EditRole)
        if isinstance(editor, QtWidgets.QLineEdit):
            model.setData(index, editor.text(), QtCore.Qt.EditRole)

    def updateEditorGeometry(self, editor: QtWidgets.QWidget, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
        editor.setGeometry(option.rect)


class SqlAlchemyTableModel(QtCore.QAbstractTableModel):
    def __init__(self, **kwargs):
        super().__init__()

    def rowCount(self, *args):
        return len(self.table_data)

    def columnCount(self, *args):
        return len(self.headers)

    def data(self, index, role):
        row = index.row()
        col = index.column()

        # EditRole is handled by SqlAlchemyItemDelegate
        if role == QtCore.Qt.EditRole:
            return self.table_data[row][col]

        # For Booleans, display checkbox
        if isinstance(self.table_data[row][col], bool):
            if role == QtCore.Qt.CheckStateRole:
                if self.table_data[row][col]:
                    return QtCore.QVariant(QtCore.Qt.Checked)
                else:
                    return QtCore.QVariant(QtCore.Qt.Unchecked)
            return self.table_data[row][col]

        # For DateTime, display QDateTime
        if isinstance(self.table_data[row][col], datetime.datetime) and role == QtCore.Qt.DisplayRole:
            return  QtCore.QDateTime(self.table_data[row][col])

        # For other data, display itself
        if role == QtCore.Qt.DisplayRole:
            return self.table_data[row][col]

    def headerData(self, section, orientation, role):
        if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal:
            return self.headers[section]

    def flags(self, index):
        if self.editable_columns and index.column() in self.editable_columns and isinstance(self.table_data[index.row()][index.column()], bool):
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable
        if self.editable_columns and index.column() in self.editable_columns and isinstance(self.table_data[index.row()][index.column()], (str, datetime.datetime)):
            return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
        return super().flags(index)

    def setData(self, index, value, role):
        row = index.row()
        col = index.column()

        if role == QtCore.Qt.EditRole:
            self.table_data[row][col] = value
            self.dataChanged.emit(index, index)
            return True
        return super().setData(index, value, role)

    def from_complex_query(self, table_data, headers, editable_columns=None):
        self.table_data = [list(row) for row in table_data]
        self.headers = headers
        self.editable_columns = editable_columns


if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    win = Gui()
    app.exec()

Upvotes: 0

Views: 301

Answers (1)

musicamante
musicamante

Reputation: 48260

The problem was not (completely) on the delegate, but in an incomplete implementation of the model: flags() should also return QtCore.Qt.ItemIsSelectable in order to allow (and display) selections.

    def flags(self, index):
        if self.editable_columns and index.column() in self.editable_columns and isinstance(self.table_data[index.row()][index.column()], bool):
            return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | 
                QtCore.Qt.ItemIsSelectable)
        if self.editable_columns and index.column() in self.editable_columns and isinstance(self.table_data[index.row()][index.column()], (str, datetime.datetime)):
            return (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | 
                QtCore.Qt.ItemIsSelectable)
        return super().flags(index)

Then, using QItemDelegate, drawBackground() is required too:

    def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
        check_state = index.data(QtCore.Qt.CheckStateRole)
        if isinstance(check_state, int):
            self.drawBackground(painter, option, index)
            # ...

Note that your code has other issues I'm not able to address right now. For instance, setData() should provide the role as optional argument (in fact, your code throws an error when clicking on the checkbox), and changing a value that is used for a checkstate with setData should use the appropriate role.

Upvotes: 1

Related Questions