Search code examples
pythonqtableviewpyside2qabstracttablemodelqstyleditemdelegate

QAbstractTableModel & QTableView with more than one StyledItemDelegateForColumn crashes my app


I have QTableView with QAbstractTableModel and multiple QStyledItemDelegates.

I set these delegates by setStyledItemForColumn.

In this case, my app crashes.

Crash happens when I push 1 key, or try to expand the gui to right.

But if I use one of them, my app goes well.

I think this is a kind of Qt bug.

Do you know somewhat?

from PySide2 import QtWidgets
from PySide2 import QtCore
from PySide2 import QtGui
from PySide2 import QtSql
import os
import PySide2
import sys
dirname = os.path.dirname(PySide2.__file__)
plugin_path = os.path.join(dirname, 'plugins', 'platforms')
os.environ['QT_QPA_PLATFORM_PLUGIN_PATH'] = plugin_path
alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"]
class IconDelegate(QtWidgets.QStyledItemDelegate):
    def initStyleOption(self, option, index):
        super(IconDelegate, self).initStyleOption(option, index)
        if option.features & QtWidgets.QStyleOptionViewItem.HasDecoration:
            s = option.decorationSize
            s.setWidth(option.rect.width())
            option.decorationSize = s
class Delegate(QtWidgets.QStyledItemDelegate):
    def __init__(self, parent=None):
        super(Delegate, self).__init__(parent=None)
    def initStyleOption(self, option, index):

#        super(IconDelegate, self).initStyleOption(option, index)
        if index.column() == 6:
            if option.features & QtWidgets.QStyleOptionViewItem.HasDecoration:
                s = option.decorationSize
                s.setWidth(option.rect.width())
                option.decorationSize = s
    def createEditor(self, parent, option, index):
        editor = QtWidgets.QComboBox(parent)

        return editor
    def setEditorData(self, editor, index):
        model = index.model()
        items = model.items
        text = items[index.row()][index.column()]
        editor.setCurrentText(text)
    def setModelData(self, editor, model, index):

        items = model.items
#
class TableView(QtWidgets.QTableView):
    def __init__(self, parent=None):
        super(TableView, self).__init__(parent=None)
        delegate = Delegate()
        self.setItemDelegate(delegate)
        #Here is the crash point
#        self.setItemDelegateForColumn(6, delegate)
#        self.setItemDelegateForColumn(11, IconDelegate())
        self.tableModel = TableModel(2, 15)
        self.setModel(self.tableModel)
    def keyPressEvent(self, event):
        if event.key() == QtCore.Qt.Key_1:
            self.tableModel.insertRows(0)

class TableItem(object):
    def __init__(self,  parent=None):        
        self.root = False

        self.word = ""
        self.alignment = QtCore.Qt.AlignCenter

        self.rule = ""
        self.foregroundcolor = QtGui.QColor(QtCore.Qt.black)
        self.backgroundcolor = QtGui.QColor(QtCore.Qt.white)
        self.font = QtGui.QFont("Meiryo", 14)
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, row = 0, column = 0, parent = None):
        super(TableModel, self).__init__(parent = None)        
        self.items = [[TableItem() for c in range(column)] for r in range(row)]
        self.root = QtCore.QModelIndex()        
    def rowCount(self, parent=QtCore.QModelIndex()):        
        return len(self.items)
    def columnCount(self, parent=QtCore.QModelIndex()):       
        return 15
    def data(self, index, role = QtCore.Qt.DisplayRole):    
        if not index.isValid():
            return None
        row = index.row()
        column = index.column()        
        if role == QtCore.Qt.DisplayRole:
            item = self.items[row][column]
            return item
    def headerData(self, section, orientation, role = QtCore.Qt.DisplayRole):
        if orientation == QtCore.Qt.Orientation.Horizontal:
            if role == QtCore.Qt.DisplayRole:
                return alphabet[section]
        return super(TableModel, self).headerData(section, orientation, role)
    def flags(self, index):        
        return QtCore.Qt.ItemFlag.ItemIsEditable|QtCore.Qt.ItemFlag.ItemIsEnabled|QtCore.Qt.ItemFlag.ItemIsSelectable
    def setData(self, index, value, role=QtCore.Qt.EditRole):        
        if role == QtCore.Qt.EditRole:       
            row, column = index.row(), index.column()

            self.items[row][column]  = value
            self.dataChanged.emit(index, index)
            return True
        elif role == QtCore.Qt.DisplayRole:             
            row, column = index.row(), index.column()
            self.items[row][column]  = value
            self.dataChanged.emit(index, index)
            return True
        elif role == QtCore.Qt.FontRole:
            string = value.toString()
            s = string.split(",")
            font = s[0]

            self.dataChanged.emit(index, index)
            return True
    def insertRows(self, position, rows=1, index=QtCore.QModelIndex()):   
        self.beginInsertRows(QtCore.QModelIndex(), position, position+rows-1)
        for row in range(rows):        
            self.items.insert(position+row, [TableItem() for c in range(self.columnCount())])
        self.endInsertRows()
        self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
        self.emit(QtCore.SIGNAL("layoutChanged()"))      
        return True
    def removeRows(self, position, rows=1, index=QtCore.QModelIndex()):
        self.beginRemoveRows(QtCore.QModelIndex(), position, position+rows-1)
        for row in range(rows):
            self.items = self.items[:position] + \
                        self.items[position + rows:]
        self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
        self.emit(QtCore.SIGNAL("layoutChanged()"))      
        return True

def main():
    if QtWidgets.QApplication.instance() is not None:
        app = QtWidgets.QApplication.instance()
    else:
        app = QtWidgets.QApplication([])
    mainwindow = TableView()
    mainwindow.show()
    sys.exit(QtWidgets.QApplication.exec_())
if __name__ == "__main__":
    main()

Solution

  • Explanation

    It is not a Qt bug but it is a bug of your own code.

    First of all it is recommended that you run your code from the CMD / console so that you get error information, if you do you will see that the error message is:

    Traceback (most recent call last):
      File "main.py", line 38, in setEditorData
        editor.setCurrentText(text)
    TypeError: 'PySide2.QtWidgets.QComboBox.setCurrentText' called with wrong argument types:
      PySide2.QtWidgets.QComboBox.setCurrentText(TableItem)
    Supported signatures:
      PySide2.QtWidgets.QComboBox.setCurrentText(str)
    

    The error clearly indicates that the setCurrentText method expects a string but is receiving a TableItem. Why do you receive a TableItem? Well with your code items[index.row()][index.column()] returns a TableItem, assuming you want to get the text "word" then you must use:

    def setEditorData(self, editor, index):
        model = index.model()
        items = model.items
        item = items[index.row()][index.column()]
        text = item.word
        editor.setCurrentText(text)

    In both cases (setItemDelegate or setItemD) it causes the error.

    But the error still persists when the window is resized since it is caused by the other delegate. Since you are partially overriding a delegate then the other party continues to use the generic information, for example expect index.data(Qt.DisplayRole) to return a string but in your case return a TableItem:

    def data(self, index, role = QtCore.Qt.DisplayRole):    
        if not index.isValid():
            return None
        row = index.row()
        column = index.column()        
        if role == QtCore.Qt.DisplayRole:
            item = self.items[row][column]
            return item

    In conclusion, the OP have not correctly used the default roles, causing delegates who use this information to obtain the incorrect data.

    Solution

    Considering all of the above, I have corrected many errors that I have not mentioned previously because many are trivial or are out of topic, obtaining the following code:

    from PySide2 import QtWidgets
    from PySide2 import QtCore
    from PySide2 import QtGui
    from PySide2 import QtSql
    import os
    import PySide2
    import sys
    
    dirname = os.path.dirname(PySide2.__file__)
    plugin_path = os.path.join(dirname, "plugins", "platforms")
    os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = plugin_path
    
    alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"]
    
    
    class IconDelegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            super(IconDelegate, self).initStyleOption(option, index)
            if option.features & QtWidgets.QStyleOptionViewItem.HasDecoration:
                s = option.decorationSize
                s.setWidth(option.rect.width())
                option.decorationSize = s
    
    
    class Delegate(QtWidgets.QStyledItemDelegate):
        def initStyleOption(self, option, index):
            if option.features & QtWidgets.QStyleOptionViewItem.HasDecoration:
                s = option.decorationSize
                s.setWidth(option.rect.width())
                option.decorationSize = s
    
        def createEditor(self, parent, option, index):
            editor = QtWidgets.QComboBox(parent)
            return editor
    
    
    #
    class TableView(QtWidgets.QTableView):
        def __init__(self, parent=None):
            super(TableView, self).__init__(parent=None)
            delegate = Delegate(self)
            # self.setItemDelegate(delegate)
            # Here is the crash point
            self.setItemDelegateForColumn(6, delegate)
            icon_delegate = IconDelegate(self)
            self.setItemDelegateForColumn(11, icon_delegate)
            self.tableModel = TableModel(2, 15)
            self.setModel(self.tableModel)
    
        def keyPressEvent(self, event):
            if event.key() == QtCore.Qt.Key_1:
                self.tableModel.insertRows(0)
    
    
    class TableItem(object):
        def __init__(self, parent=None):
            self.root = False
    
            self.word = ""
            self.alignment = QtCore.Qt.AlignCenter
    
            self.rule = ""
            self.foregroundcolor = QtGui.QColor(QtCore.Qt.black)
            self.backgroundcolor = QtGui.QColor(QtCore.Qt.white)
            self.font = QtGui.QFont("Meiryo", 14)
    
    
    class TableModel(QtCore.QAbstractTableModel):
        def __init__(self, row=0, column=0, parent=None):
            super(TableModel, self).__init__(parent=None)
            self.items = [[TableItem() for c in range(column)] for r in range(row)]
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            return len(self.items)
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            if self.items:
                return len(self.items[0])
            return 0
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if not index.isValid():
                return None
            row = index.row()
            column = index.column()
            if 0 <= row < self.rowCount() and 0 <= column < self.columnCount():
                item = self.items[row][column]
            if role == QtCore.Qt.DisplayRole:
                text = item.word
                return text
            elif role == QtCore.Qt.EditRole:
                return item
    
        def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
            if orientation == QtCore.Qt.Orientation.Horizontal:
                if role == QtCore.Qt.DisplayRole and section < len(alphabet):
                    return alphabet[section]
            return super(TableModel, self).headerData(section, orientation, role)
    
        def flags(self, index):
            return (
                QtCore.Qt.ItemFlag.ItemIsEditable
                | QtCore.Qt.ItemFlag.ItemIsEnabled
                | QtCore.Qt.ItemFlag.ItemIsSelectable
            )
    
        def setData(self, index, value, role=QtCore.Qt.EditRole):
            if role == QtCore.Qt.EditRole:
                row, column = index.row(), index.column()
                self.items[row][column].word = value
                self.dataChanged.emit(index, index)
                return True
            elif role == QtCore.Qt.DisplayRole:
                row, column = index.row(), index.column()
                self.items[row][column].word = value
                self.dataChanged.emit(index, index)
                return True
            return False
    
        def insertRows(self, position, rows=1, index=QtCore.QModelIndex()):
            self.beginInsertRows(QtCore.QModelIndex(), position, position + rows - 1)
            for row in range(rows):
                self.items.insert(
                    position + row, [TableItem() for c in range(self.columnCount())]
                )
            self.endInsertRows()
            self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
            self.emit(QtCore.SIGNAL("layoutChanged()"))
            return True
    
        def removeRows(self, position, rows=1, index=QtCore.QModelIndex()):
            self.beginRemoveRows(QtCore.QModelIndex(), position, position + rows - 1)
            for row in range(rows):
                self.items = self.items[:position] + self.items[position + rows :]
            self.emit(QtCore.SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
            self.emit(QtCore.SIGNAL("layoutChanged()"))
            return True
    
    
    def main():
        if QtWidgets.QApplication.instance() is not None:
            app = QtWidgets.QApplication.instance()
        else:
            app = QtWidgets.QApplication([])
        mainwindow = TableView()
        mainwindow.show()
        sys.exit(QtWidgets.QApplication.exec_())
    
    
    if __name__ == "__main__":
        main()