Как правильно обновить представление в pyQt после редактирования абстрактной модели в другом потоке? - PullRequest
1 голос
/ 16 июня 2019

Я пытаюсь установить setData () моего QAbstractTableModel (который подключен к QTableView) из другого потока. Данные в модели изменяются, как и ожидалось, но представление не обновляется само по себе (только после нажатия на представление таблицы, которое вызывает представление для обновления). Каков наилучший способ реализации такого обновления?

Я работаю над Python 3.6 с pyqt 5.11.1. Я пытался испустить dataChanged (а также layoutAboutToBeChanged, layoutChanged, editCompleted) сигнал от метода setData моей модели - ничего из этого не работает. Затем я предложил два возможных решения: 1) испускающий modelReset из setData или же 2) создание QTimer в модели и подключение его к методу, который генерирует dataChanged для всех индексов модели

И то и другое работает, как и ожидалось, но я думаю, что это не очень хорошие решения, так как сначала нужно обновить всю таблицу (я так полагаю), и это не очень полезный вариант? И второе решение просто даст постоянную нагрузку на приложение, за исключением некоторой задержки отображения данных.

Это минимальный (надеюсь, так), воспроизводимый пример моей проблемы


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_()

Индекс (0, 0) табличного представления должен обновляться каждую секунду с увеличением счетчика (что не происходит, когда я пытаюсь передать сигнал dataChanged, работает только с modelReset). (обратите внимание, это лишь минимальный пример потока, который имеет более сложную логику в реальном коде и данные не поступают «по таймеру»)

Таймер от https://github.com/Taar2/pyqt5-modelview-tutorial/blob/master/modelview_3.py также заставляет его работать (минусы этого решения описаны выше).

Я ожидаю, что сигналы будут работать так же, но по какой-то причине это не происходит, и представление не обновляется с помощью сигнала dataChanged, вызванного из потока.

1 Ответ

1 голос
/ 16 июня 2019

Непригоден прямой доступ к модели из другого потока, поскольку объекты QObject не являются поточно-ориентированными, вместо этого он создает объект QObject, который отправляет данные в основной поток через сигналы, в данном случае для простой операции, которую я создал, слот update_item, который получает строку, столбец и данные.

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_()
...