Search code examples
pythonpython-3.xpyqtpyqt5python-multithreading

How to correctly update view in pyQt after editing abstract model in another thread?


I'm trying to setData() of my QAbstractTableModel (which is connected to QTableView) from another threading.Thread. Data in model changing as expected, but view isn't updating by itself (only after clicking on table view which provokes view to update). What's the best way of implementing such update?

I'm working on Python 3.6 with pyqt 5.11.1. I've tried to emit dataChanged (as well as layoutAboutToBeChanged, layoutChanged, editCompleted)signal from setData method of my model - none of that works. Then I came up with two possible solutions -

  1. emitting modelReset from setData or
  2. making QTimer in model and connecting it to method that emitting dataChanged for all indexes of the model

Both of that works as expected, but I think that this is not really good solutions as first making the whole table to update (I believe so) and it's not really healthy use case for it? And the second solution will just give a constant load on app aside of some delay of displaying data.

That's minimal (hope so) reproducible example of my problem


import sys
import threading
import time

from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt as Qt


class CopterDataModel(QtCore.QAbstractTableModel):
    def __init__(self, parent=None):
        super(CopterDataModel, self).__init__(parent)
        self.data_contents = [[1, 2]]

    def rowCount(self, n=None):
        return len(self.data_contents)

    def columnCount(self, n=None):
        return 2

    def data(self, index, role):
        row = index.row()
        col = index.column()
        #print('row {}, col {}, role {}'.format(row, col, role)) #for debug
        if role == Qt.DisplayRole:
            return self.data_contents[row][col] or ""


    @QtCore.pyqtSlot()
    def setData(self, index, value, role=Qt.EditRole):
        if not index.isValid():
            return False

        if role == Qt.EditRole:
            self.data_contents[index.row()][index.column()] = value
            print("edit", value)

            self.modelReset.emit() # working fine
            #self.dataChanged.emit(index, index, [Qt.EditRole]) # NOT WORKING

        else:
            return False

        return True

    def flags(self, index):
        roles = Qt.ItemIsSelectable | Qt.ItemIsEnabled
        return roles

if __name__ == '__main__':

    def timer():
        idc = 1001
        while True:
            myModel.setData(myModel.index(0, 0), idc)
            idc += 1
            time.sleep(1)

    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)

    tableView = QtWidgets.QTableView()
    myModel = CopterDataModel(None)

    tableView.setModel(myModel)

    tableView.show()

    t = threading.Thread(target=timer, daemon=True)
    t.start()

    app.exec_()

Index (0, 0) of table view should be updating every second with incrementing counter (which not happening when I trying to emit dataChanged signal, only working with modelReset). (please note, that's just minimal example of thread which have more complex logic in real code, and data not incoming "at timer")

Timer tweak from https://github.com/Taar2/pyqt5-modelview-tutorial/blob/master/modelview_3.py also making it work (cons of that solution described above).

I expect signals to work the same way, but for some reason it's not happening and view doesn't updates with dataChanged signal called from thread.


Solution

  • It is not good to access the model directly from another thread since the QObjects are not thread-safe, instead it creates a QObject that sends the data to the main thread through signals, in this case for a simple operation I created the slot update_item that receives the row, column and data.

    import sys
    import threading
    import time
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class CopterDataModel(QtCore.QAbstractTableModel):
        def __init__(self, parent=None):
            super(CopterDataModel, self).__init__(parent)
            self.data_contents = [[1, 2]]
    
        def rowCount(self, n=None):
            return len(self.data_contents)
    
        def columnCount(self, n=None):
            return 2
    
        def data(self, index, role):
            row = index.row()
            col = index.column()
            # print('row {}, col {}, role {}'.format(row, col, role)) #for debug
            if role == QtCore.Qt.DisplayRole:
                return self.data_contents[row][col] or ""
    
        def setData(self, index, value, role=QtCore.Qt.EditRole):
            if not index.isValid():
                return False
    
            if role == QtCore.Qt.EditRole:
                self.data_contents[index.row()][index.column()] = value
                print("edit", value)
                self.dataChanged.emit(
                    index, index, (QtCore.Qt.EditRole,)
                )  # NOT WORKING
            else:
                return False
            return True
    
        def flags(self, index):
            return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
    
        @QtCore.pyqtSlot(int, int, QtCore.QVariant)
        def update_item(self, row, col, value):
            ix = self.index(row, col)
            self.setData(ix, value)
    
    
    class SignalManager(QtCore.QObject):
        fooSignal = QtCore.pyqtSignal(int, int, QtCore.QVariant)
    
    
    if __name__ == "__main__":
    
        def timer(obj):
            idc = 1001
            while True:
                obj.fooSignal.emit(0, 0, idc)
                idc += 1
                time.sleep(1)
    
        app = QtWidgets.QApplication.instance()
        if app is None:
            app = QtWidgets.QApplication(sys.argv)
    
        foo = SignalManager()
    
        tableView = QtWidgets.QTableView()
        myModel = CopterDataModel()
        foo.fooSignal.connect(myModel.update_item)
    
        tableView.setModel(myModel)
    
        tableView.show()
    
        t = threading.Thread(target=timer, args=(foo,), daemon=True)
        t.start()
    
        app.exec_()