qtqmlpysidepyside6qabstracttablemodel

How to set a QAbstractTableModel custom class model to the QML TableView


I have created a custom table model Python class, subclassing the QAbstractTableModel and when I try to set the instance of this class, model, to the model property of the TableView, the whole app crashes. There are no error debug info in the terminal about what is causing the crash. I am using QML with version Qt 6.4 and PySide6.

code:

main.py:

# This Python file uses the following encoding: utf-8
import sys
import os
from PySide6.QtCore import QUrl, QObject, Slot, Qt, QAbstractTableModel
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine, QJSValue, qmlRegisterType
import create_table_model_E1


class Comm(QObject):
    '''
    Object - slot-owner and signal acceptor
    '''

    def __init__(self):
        super().__init__()

    # Signal reciever for signal rowColData from qml that contains header data for table
    @Slot(QJSValue, QJSValue)
    def handle_row_col_data(self, row_names: QJSValue, col_names: QJSValue):
        row_names = row_names.toVariant()
        col_names = col_names.toVariant()
        print("Signal received from QML - Row Names:", row_names)
        print("Signal received from QML - Column Names:", col_names)
        print("Creating Table Model..")
        model = create_table_model_E1.create_model(row_names, col_names, False)
        if isinstance(model, QAbstractTableModel):
            tableView.setProperty("model", model)
            # pass

## tried, importing it in qml file doesnt work
# # Register the CustomTableModel with QML
# sys.path.append('create_table_model_E1.py')
# qmlRegisterType(create_table_model_E1.CustomTableModel, "myCustomTableModel", 1, 0,
#                                        "MyCustomTableModel")
##

if __name__ == '__main__':
    # Create a QApplication instance
    app = QGuiApplication(sys.argv)

    # Get the absolute path to the QML file
    qml_file = os.path.abspath('content/App.qml')

    # Reciever class
    com = Comm()

    # Create a QQmlApplicationEngine instance
    engine = QQmlApplicationEngine()

    # Load the main QML file
    engine.load(QUrl.fromLocalFile(qml_file))
    
    qml_obj = engine.rootObjects()[0]
    
    # find the object that emits the signal containing the data
    rowColData_comp = qml_obj.findChild(QObject, "data_to_table")
    rowColData_comp.rowColData.connect(com.handle_row_col_data,
                                       type=Qt.ConnectionType.QueuedConnection)

    # find the tableview component
    tableView = qml_obj.findChild(QObject, "TableView")

    # If the rootObjects() method of the QQmlApplicationEngine
    # instance returns an empty list,
    # it means the QML file could not be loaded, so exit the
    # application with a status code of -1
    if not engine.rootObjects():
        sys.exit(-1)

    # Start the main event loop of the application by calling app.exec()
    sys.exit(app.exec())

create_table_model_E1.py:

class CustomTableModel(QAbstractTableModel):

    dataChanged = Signal(QModelIndex, QModelIndex, list)

    def __init__(self, data, headers, parent=None) -> None:
        super(CustomTableModel, self).__init__(parent)
        self._data = data
        self._headers = headers

    def rowCount(self, parent=None) -> int:
        # Return the number of rows in the table
        return len(self._data)

    def columnCount(self, parent=None) -> int:
        # Return the number of columns in the table
        return len(self._data[0])

    def data(self, index: Union[QModelIndex, QPersistentModelIndex], role=Qt.DisplayRole) -> Any:
        # Return the data for a specific index and role
        if not index.isValid():
            return None

        row = index.row()
        col = index.column()

        if role == Qt.DisplayRole:
            # Return the display data for the cell
            return self._data[row][col]

    def headerData(self, section, orientation: Qt.Orientation, role=Qt.DisplayRole) -> Any:
        # Return the header data for a specific section and role
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                # Return the horizontal header data
                return self._headers[section]
            elif orientation == Qt.Vertical:
                # Return the vertical header data
                return str(section + 1)

    # ... editable model methods ...
    def setData(self, index: Union[QModelIndex, QPersistentModelIndex], value: Any, role: int = Qt.EditRole) -> bool:  # noqa
        if role == Qt.EditRole:
            row = index.row()
            column = index.column()
            if 0 <= row < self.rowCount() and 0 <= column < self.columnCount():
                # Update the data in the internal data structure
                self._data[row][column] = value
                # Emit dataChanged signal to notify the view
                self.dataChanged.emit(index, index, [role])
                return True
        return False

    def flags(self, index: Union[QModelIndex, QPersistentModelIndex]) -> Qt.ItemFlag:
        default_flags = super().flags(index)
        if index.isValid():
            # Set the item flags to be editable
            return default_flags | Qt.ItemIsEditable
        return default_flags

    # ... other methods ...

    def insertRows(self, row, count, parent=QModelIndex()) -> bool:
        self.beginInsertRows(parent, row, row + count - 1)
        for _ in range(count):
            empty_row = [' '] * self.columnCount()
            self._data.insert(row, empty_row)
        self.endInsertRows()
        return True

    def insertColumns(self, column, count, parent=QModelIndex()) -> bool:
        self.beginInsertColumns(parent, column, column + count - 1)
        for _ in range(count):
            for row in self._data:
                row.insert(column, ' ')
            self._headers.insert(column, ' ')
        self.endInsertColumns()
        return True

    def removeRows(self, row, count, parent=QModelIndex()) -> bool:
        self.beginRemoveRows(parent, row, row + count - 1)
        for _ in range(count):
            self._data.pop(row)
        self.endRemoveRows()
        return True

    def removeColumns(self, column, count, parent=QModelIndex()) -> bool:
        self.beginRemoveColumns(parent, column, column + count - 1)
        for row in self._data:
            for _ in range(count):
                row.pop(column)
            self._headers.pop(column)
        self.endRemoveColumns()
        return True

    def getAllData(self) -> list:
        """
        Return all data in the model.
        """
        return self._data


def create_model(rowsn: list, colsn: list, isEl3: bool = True) -> QAbstractTableModel:

    rownames = ['names']
    colnames = ['names']

    for col in colsn:
        colnames.append(col)
    for row in rowsn:
        rownames.append(row)

    rownames.append('Weights')
    if isEl3:
        rownames.append('Indifference(q)')
        rownames.append('Preference(p)')

    rownames.append('Veto')

    data = []
    temp = []

    for rid, row in enumerate(rownames):
        for id, col in enumerate(colnames):
            if rid == 0:
                temp.append(col)
            else:
                if id == 0:
                    temp.append(row)
                else:
                    temp.append(' ')
        data.append(copy(temp))
        temp.clear()

    model = CustomTableModel(data, data[0])

    return model

Table.qml:

import QtQuick
import QtQuick.Controls 2.15
// import myCustomTableModel 1.0 doesnt work

Item {
    id: root
    width: 400
    height: 200
    property var colNames: ["names", "cr1", "cr2", "cr3"]
    property var tableSize: [view.rowHeightProvider, view.columnWidthProvider]

    TableView {
        id: view
        objectName: "TableView"
        anchors.left: parent.left
        anchors.right: parent.right
        anchors.top: parent.top
        anchors.bottom: parent.bottom
        boundsBehavior: Flickable.StopAtBounds
        focus: true
        interactive: true
        anchors.topMargin: 0
        rowSpacing: -1
        columnSpacing: -1
        clip: false
        property bool tabpressed: false
        property bool returnpressed: false
        property bool uppressed: false
        property bool downpressed: false
        property bool leftpressed: false
        property bool rightpressed: false

        // user-specific table model
        property var userTableModel: null

        rowHeightProvider: function (index) {
            return 30
        }
        columnWidthProvider: function (index) {
            return 100
        }

        selectionModel: ItemSelectionModel {
            id: itemSelectionModel
            model: view.model
        }

        model: TableModelCustom {  // dummy model
            id: model
        }

        delegate: Table_customDelegate_FROM_UPWORK {
            id: viewdelegate
            width: 100
            height: 30
        }

        onUserTableModelChanged: {
            console.log("[Table_custom.qml]: USER TABLE MODEL CHANGED")
            if (view.userTableModel != null) {
                view.model = view.userTableModel
                console.log("NEW MODEL: " + view.userTableModel)
             // SOMEWHERE HERE IT CRASHES
            }
        }
    }

    HorizontalHeaderView {
        id: horizontalheader
        x: 0
        y: 0

        width: view.width
        height: 30
        boundsBehavior: Flickable.StopAtBounds
        interactive: true
        clip: true

        model: colNames

        delegate: Item {
            id: wrapper
            implicitWidth: 100
            implicitHeight: 30

            Rectangle {
                id: background
                color: "#dbdbdb"
                border.color: "#000000"
                border.width: 1
                anchors.fill: parent

                Rectangle {
                    id: rectangle
                    x: 1
                    width: 1
                    color: index === 0 ? "#222222" : "#00ffffff"
                    border.width: 0
                    anchors.top: parent.top
                    anchors.bottom: parent.bottom
                    anchors.bottomMargin: 0
                    anchors.topMargin: 0
                }
            }

            Text {
                id: text1
                color: "#3b3b3b"
                horizontalAlignment: Text.AlignHCenter
                verticalAlignment: Text.AlignVCenter
                anchors.rightMargin: 2
                anchors.bottomMargin: 2
                anchors.topMargin: 2
                text: horizontalheader.model[row]
                anchors.fill: parent
            }
        }
        syncView: view
    }
}

I tried creating the model, returning it to main.py, and then passing it to TableView using setProperty, directly, or by setting the property var userTableModel, and then setting this to the model property of TableView, but when the model cahnges, the whole app crashes. All these get done on runtime. I checked the tableView varible in main.py, and it is not a widget type(QTableView), it is QuickItemType(tableView.isQuickItemType() returns True) and thats all I could find about this. What am I missing? Why on changing the model from the existing to the newly created crashes the app?

In summary: I want to change the model of the TableView,on runtime, and when i do it, the app crashes.

Sorry for the long code blocks.


Solution

  • How about registering the model class as type and just use it in the qml file?

    Call

    qmlRegisterType(CustomTableModel, 'CustomTableModel', 1, 0, 'CustomTableModel')
    

    in your main.py.

    Then you can import this type in the Table.qml file and use the model:

    import CustomTableModel
    

    No need for the additional property userTableModel. If you want to fill the data on the Python side, register an instance of the model as singleton with qmlRegisterSingletonInstance.