Search code examples
pythonpyqtpyqt5subclassingqsqltablemodel

QSqlTableModel data edit (subclassing) problem


I'm in trouble with a small application I'm trying to write in Python 3.7 and PyQt5.

I'm quite new to Python and my knowledge on C++ is near to 0 (zero).
At the end, my project will have a Tableview whose model is a QsqlTableModel linked to a QSlite database table.

That table is composed of three columns. I'm interested in editing only the 2nd column (index 1) that has 0/1 value. In this column I'd like to put, as delegate (but with persisten editor), a switch button like here to update the boolean value (at the end the application will be something like a ToDo list).

But the problem arise just at the beginning. I'm able to populate the model and the view displays correctly the data from the table, formatting is ok (just for test purpose), but when I try to edit the data, it wont change the data in the model all the value in the edited row disappear and I get exclamation point in the row header.

This appens only of I reimplement the data method. If I reimplement only the flags method everythings ok.

I'm sure I'm making a stupid error, but I can't find the problem...

Here is my code.

main.py

from MyClass import MyConnection, TableView, Model
from PyQt5 import QtWidgets, QtCore, QtGui
import sys


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    MyConnection('data22.db')
    model = Model('tabella_di_test')
    table = TableView(model)
    table.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
    table.show()
    sys.exit(app.exec())

MyClass.py

from PyQt5.QtSql import QSqlDatabase, QSqlTableModel
from PyQt5.QtWidgets import QTableView, QItemDelegate
from PyQt5.QtCore import Qt, QModelIndex, QRect, QAbstractTableModel, QVariant
from PyQt5.QtGui import QFont, QPainter, QPixmap
import typing
import sys

COL_DEL = 1 #column that receive delegate in the future
COL_ICON = 2


class MyConnection:
    def __init__(self, name):
        super(MyConnection, self).__init__()
        self.db = QSqlDatabase().addDatabase("QSQLITE")
        self.db.setDatabaseName(name)
        if self.db.open():
            print("Connection Opened...")
        else:
            sys.exit("Errore nella connessione al DB...")


class TableView(QTableView):
    def __init__(self, model):
        super(TableView, self).__init__()
        self.setAlternatingRowColors(True)
        self.setModel(model)


class Model(QSqlTableModel):
    def __init__(self, name):
        super(Model, self).__init__()
        self.setTable(name)
        self.select()
        print(self.rowCount())

    def flags(self, index: QModelIndex):
        if not index.isValid():
            print('Flags ==> Index is not valid!')
            return QVariant()
        if index.column() == COL_DEL:
            print('Flags ==> Index({},{}) '.format(index.row(), index.column()))
            return Qt.NoItemFlags | Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
        else:
            return Qt.NoItemFlags | Qt.ItemIsEnabled

    def data(self, index: QModelIndex, role=None) -> typing.Any:
        if not index.isValid():
            print('Index is not valid!')
            return QVariant()
        if role == Qt.TextAlignmentRole:
            return Qt.AlignCenter
        if role == Qt.DisplayRole:
            return super(Model, self).data(index, role)
        if role == Qt.FontRole and index.column() == COL_DEL:
            font = QFont()
            font.setBold(True)
            font.setPixelSize(16)
            return font


class Delegate(QItemDelegate):
    def __init__(self):
        super(Delegate, self).__init__()

Solution

  • No need to implement a custom model for this case, just a QSqlTableModel with a custom delegate is enough:

    import os
    
    from PyQt5 import QtCore, QtGui, QtWidgets, QtSql
    
    CURRENT_DIR = os.path.dirname(os.path.realpath(__file__))
    COL_DEL = 1
    
    
    def create_connection(name):
        db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(name)
        if db.open():
            print("Connection Opened...")
            return True
        print("Errore nella connessione al DB...")
        return False
    
    
    class MySwitch(QtWidgets.QPushButton):
        def __init__(self, parent=None):
            super().__init__(parent)
            self.setCheckable(True)
            self.setMinimumWidth(66)
            self.setMinimumHeight(22)
    
        def paintEvent(self, event):
            label = "ON" if self.isChecked() else "OFF"
            bg_color = (
                QtGui.QColor(QtCore.Qt.green)
                if self.isChecked()
                else QtGui.QColor(QtCore.Qt.red)
            )
    
            radius = 10
            width = 32
            center = self.rect().center()
    
            painter = QtGui.QPainter(self)
            painter.setRenderHint(QtGui.QPainter.Antialiasing)
            painter.translate(center)
            painter.setBrush(QtGui.QColor(0, 0, 0))
    
            pen = QtGui.QPen(QtCore.Qt.black)
            pen.setWidth(2)
            painter.setPen(pen)
    
            painter.drawRoundedRect(
                QtCore.QRect(-width, -radius, 2 * width, 2 * radius), radius, radius
            )
            painter.setBrush(QtGui.QBrush(bg_color))
            sw_rect = QtCore.QRect(-radius, -radius, width + radius, 2 * radius)
            if not self.isChecked():
                sw_rect.moveLeft(-width)
            painter.drawRoundedRect(sw_rect, radius, radius)
            painter.drawText(sw_rect, QtCore.Qt.AlignCenter, label)
    
    
    class CustomDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super().initStyleOption(option, index)
            font = QtGui.QFont()
            font.setBold(True)
            font.setPixelSize(16)
            option.font = font
            option.displayAlignment = QtCore.Qt.AlignCenter
    
        def paint(self, painter, option, index):
            if (
                isinstance(option.widget, QtWidgets.QAbstractItemView)
                and index.column() == COL_DEL
            ):
                option.widget.openPersistentEditor(index)
                return
            super().paint(painter, option, index)
    
        def createEditor(self, parent, option, index):
            if index.column() == COL_DEL:
                editor = MySwitch(parent)
                editor.toggled.connect(lambda *args, e=editor: self.commitData.emit(e))
                return editor
    
        def setModelData(self, editor, model, index):
            if index.column() == COL_DEL:
                value = 1 if editor.isChecked() else 0
                model.setData(index, value)
            else:
                super().setModelData(editor, model, index)
    
        def setEditorData(self, editor, index):
            if index.column() == COL_DEL:
                value = bool(index.data())
                editor.setChecked(value)
            else:
                super().setEditorData(editor, index)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        if not create_connection(os.path.join(CURRENT_DIR, "data22.db")):
            sys.exit(-1)
    
        w = QtWidgets.QTableView()
        delegate = CustomDelegate(w)
        w.setItemDelegate(delegate)
        model = QtSql.QSqlTableModel()
        model.setEditStrategy(QtSql.QSqlTableModel.OnFieldChange)
        model.setTable("tabella_di_test")
        model.select()
        w.setModel(model)
        w.resize(640, 480)
        w.show()
    
        sys.exit(app.exec_())