Search code examples
pythonqtableviewpyside2qabstracttablemodel

QAbstractTabelModel.data() always getting index.row() == 0 and only shows 1 row


I have defined my custom QAbstrtactTableModel in python and implemented columnCount(), rowCount(), data() and headerData() and also added a function to add() new entries:

import sys
from PySide2.QtUiTools import QUiLoader
from PySide2.QtWidgets import QApplication, QMainWindow
from PySide2.QtCore import QFile, QProcess, Signal,\
    Slot, QObject, QThread, Qt, QAbstractTableModel, QModelIndex,\
    QTimer
import random


class TableModel(QAbstractTableModel):
    def __init__(self, values):
        QAbstractTableModel.__init__(self)

        self.values = values

    def columnCount(self, parent=QModelIndex()):
        return 2

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

    def data(self, index, role=Qt.DisplayRole):
        print("called")
        if 0 <= index.row() < self.rowCount() and 0 <= index.column() < self.columnCount():
            if role == Qt.DisplayRole:
                print(index.row())
                if index.column() == 0:
                    return list(self.values.keys())[index.row()]
                else:
                    return self.values[list(self.values.keys())[index.row()]]

    def add(self, data):
        self.values[data[0]] = data[1]

    def headerData(self, section, orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                if section == 0:
                    return "Parking,Meter"
                elif section == 1:
                    return "Revenue"


class Monitor(QObject):
    data_batch_ready = Signal(list)

    def __init__(self, parent=None):
        super(Monitor, self).__init__(parent)

    @Slot(list)
    def fill_batch(self):
        my_data = []
        for parking in range(4):
            for index in range(10):

                my_data.append([str(parking)+","+str(index), random.randint(1,101)])
        self.data_batch_ready.emit(my_data)


class Gui(QMainWindow):
    system_process = QProcess()
    parking_model = TableModel(values={"0,0": "0"})

    def __init__(self, parent=None):
        super(Gui, self).__init__(parent)
        file = QFile("minimal.ui")
        file.open(QFile.ReadOnly)

        loader = QUiLoader()
        ui = loader.load(file)
        self.setCentralWidget(ui)

        self.centralWidget().parking_table.setModel(self.parking_model)

    @Slot(list)
    def update_gui_revenue(self, data):
        print("fire")
        for row in data:
            self.parking_model.add(row)


def main():

    app = QApplication(sys.argv)
    gui = Gui()
    gui.show()

    thread = QThread()
    moni = Monitor()
    timer = QTimer(moni)
    timer.setInterval(1)
    moni.moveToThread(thread)
    moni.data_batch_ready.connect(gui.update_gui_revenue, Qt.QueuedConnection)
    timer.timeout.connect(moni.fill_batch, Qt.QueuedConnection)
    thread.started.connect(timer.start)
    thread.started.connect(lambda: print("time started"))
    thread.start()

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

The data is a python dictionary that is being modified by calling a random function that fills the dictionary with 1 entry every 1 second as follows:

"0,0":[random value]
"0,1":[random value]
"0,2":[random value]
"1,0":[random value]
"1,1":[random value]
"1,2":[random value]

rowCount() correctly reflects the number of rows in the data values, but the tableView only displays 1 row all the time and the data() is only requesting index.row() == 0

EDIT: added timer.setInterval(1)


Solution

  • The models can not monitor when a data is modified if you do not notify it, for which in your case there are 2 types of modifications, the first is the row insertion and the other is the update. For the insertion of rows you must use the beginInsertRows() and endInsertRows() methods, and for the second you must notify by launching the dataChanged signal.

    On the other hand, do not make the model variable in the class, you have to make it a variable of the instance.

    import sys
    from PySide2 import QtCore, QtWidgets, QtUiTools
    import random
    
    
    class TableModel(QtCore.QAbstractTableModel):
        def __init__(self, values={}, parent=None):
            super(TableModel, self).__init__(parent)
            self.values = values
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            if parent.isValid(): return 0
            return 2
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            if parent.isValid(): return 0
            return len(self.values)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
            if 0 <= index.row() < self.rowCount() and 0 <= index.column() < self.columnCount():
                if role == QtCore.Qt.DisplayRole:
                    if index.column() == 0:
                        return list(self.values.keys())[index.row()]
                    else:
                        return self.values[list(self.values.keys())[index.row()]]
    
        def add(self, data):
            key, value = data 
            if key in self.values:
                row = list(self.values.keys()).index(key)
                self.values[key] = value
                self.dataChanged.emit(self.index(row, 1), self.index(row, 1))
            else:
                self.add_row(key, value)
    
        def add_row(self, key, value):
            self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
            self.values[key] = value
            self.endInsertRows()
    
        def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
            if role == QtCore.Qt.DisplayRole:
                if orientation == QtCore.Qt.Horizontal:
                    if section == 0:
                        return "Parking,Meter"
                    elif section == 1:
                        return "Revenue"
    
    
    class Monitor(QtCore.QObject):
        data_batch_ready = QtCore.Signal(list)
    
        @QtCore.Slot(list)
        def fill_batch(self):
            my_data = []
            for parking in range(4):
                for index in range(10):
                    my_data.append([str(parking)+","+str(index), random.randint(1,101)])
            self.data_batch_ready.emit(my_data)
    
    
    class Gui(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            super(Gui, self).__init__(parent)
            file =QtCore.QFile("minimal.ui")
            file.open(QtCore.QFile.ReadOnly)
            self.parking_model = TableModel(values={"0,0": "0"}, parent=self)
    
            loader = QtUiTools.QUiLoader()
            ui = loader.load(file)
            self.setCentralWidget(ui)
    
            self.centralWidget().parking_table.setModel(self.parking_model)
    
        @QtCore.Slot(list)
        def update_gui_revenue(self, data):
            for row in data:
                self.parking_model.add(row)
    
    
    def main():
    
        app = QtWidgets.QApplication(sys.argv)
        gui = Gui()
        gui.show()
    
        thread = QtCore.QThread()
        moni = Monitor()
        timer = QtCore.QTimer(moni)
        timer.setInterval(10)
        moni.moveToThread(thread)
        moni.data_batch_ready.connect(gui.update_gui_revenue, QtCore.Qt.QueuedConnection)
        timer.timeout.connect(moni.fill_batch, QtCore.Qt.QueuedConnection)
        thread.started.connect(timer.start)
        thread.started.connect(lambda: print("time started"))
        thread.start()
        app.aboutToQuit.connect(thread.quit)
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()