Как обновить QStandartItemModel без замораживания основного пользовательского интерфейса

#python #pyqt #qthread

#python #pyqt #qthread

Вопрос:

Я начинаю изучать PyQt4 и уже давно застрял на чем-то и не могу разобраться сам:

Вот концепция: Существует TreeView с пользовательским QStandartItemModel, который перестраивается каждые пару секунд и может содержать много (как минимум сотни) записей, также будут дополнительные делегаты для разных столбцов и т.д. Это довольно сложно, и время сборки даже для простой модели без делегатов достигает 0,3 секунды, что приводит к зависанию TreeView.

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

вот некоторый код, который может немного проиллюстрировать проблему:

 from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys, os, re, time

app = QApplication(sys.argv)
REFRESH = 1

class Reloader_Thread(QThread):
    def __init__(self, parent = None):
        QThread.__init__(self, parent)
        self.loaders = ['\', '--', '|', '/', '--']
        self.emit(SIGNAL('refresh'))
    def run(self):
        format = '|%d/%b/%Y %H:%M:%S| '
        while True:
            self.emit(SIGNAL('refresh'))
            self.sleep(REFRESH)

class Model(QStandardItemModel):
    def __init__(self, viewer=None):
        QStandardItemModel.__init__(self,None)
        self.build()

    def build(self):
        stTime = time.clock()
        newRows = []
        for r in range(1000):
            row = []
            for c in range(12):
                item = QStandardItem('%s dd' % (time.strftime('%H"%M'%S'), r,c))
                row.append(item)
            newRows.append(row)
        eTime = time.clock() - stTime
        outStr = 'Build f' % eTime
        format = '|%d/%b/%Y %H:%M:%S| '
        stTime = time.clock()
        self.beginRemoveRows(QModelIndex(),  0, self.rowCount())
        self.removeRows(0, self.rowCount())
        self.endRemoveRows()
        eTime = time.clock() - stTime
        outStr  = ', Remove f' % eTime
        stTime = time.clock()
        numNew = len(newRows)
        for r in range(numNew):
            self.appendRow(newRows[r])
        eTime = time.clock() - stTime
        outStr  = ', Set f' % eTime
        self.emit(SIGNAL('status'), outStr)
        self.reset()

w = QWidget()
w.setGeometry(200,200,800,600)
hb = QVBoxLayout(w)
tv = QTreeView()
tvm = Model(tv)
tv.setModel(tvm)

sb = QStatusBar()
reloader = Reloader_Thread()
tvm.connect(tvm, SIGNAL('status'), sb.showMessage)
reloader.connect(reloader, SIGNAL('refresh'), tvm.build)
reloader.start()

hb.addWidget(tv)
hb.addWidget(sb)
w.show()
app.setStyle('plastique')
app.processEvents(QEventLoop.AllEvents)
app.aboutToQuit.connect(reloader.quit)
app.exec_()
  

Спасибо за советы.
Вот ситуация, с которой я столкнулся до сих пор:
При каждом обновлении я создаю новую модель и отправляю ее в TreeView … это быстро, но я не знаю, что происходит с текущей моделью TreeView и как с этим бороться, также кажется, что память, используемая моим «приложением», постоянно увеличивается.

Другое дело, что я хочу сохранить свой выбор, но на основе данных элемента, а не визуального прямоугольника или порядка строк, поэтому я тоже это сделал, но это выглядит слишком грязно / хакерски, чтобы быть правильным способом. Любая помощь по этому вопросу также будет оценена. Следующий код:

 from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys, os, re, time

app = QApplication(sys.argv)
REFRESH = 1

class Reloader_Thread(QThread):
    def __init__(self, parent = None):
        QThread.__init__(self, parent)
        self.moveToThread(self)

    def run(self):
        while True:
            model = Model()
            #model.connect(model, SIGNAL('status'), self.emitStat)
            if model.build():
                self.emit(SIGNAL('refresh'), model)
            self.sleep(REFRESH)

    def emitStat(self, stat):
        self.emit(SIGNAL('status'), stat)

class Tree(QTreeView):
    def __init__(self, parent=None):
        QTreeView.__init__(self, parent)
        self.setSelectionMode(QAbstractItemView.ExtendedSelection)

    def resetModel(self, model):
        stTime = time.clock()
        # gather old selection
        oldSel = set()
        currModel = self.model()
        for index in self.selectedIndexes():
            id = currModel.itemFromIndex(index).data().toString()
            oldSel.add('%s'%id)
        # setup new
        self.setModel(model)
        selModel = self.selectionModel()
        for r in range(model.rowCount()):
            item = model.item(r,0)
            rowId = '%s' % item.data().toString()
            if rowId in oldSel:
                sel = QItemSelection(model.index(r,0), model.index(r,model.columnCount()-1))
                selModel.select(sel, QItemSelectionModel.Select)
        self.setSelectionModel(selModel)
        self.emit(SIGNAL('status'), 'TV setModel: fs' % (time.clock() - stTime))

class Model(QStandardItemModel):
    def __init__(self, viewer=None):
        QStandardItemModel.__init__(self,None)

    def build(self):
        stTime = time.clock()
        newRows = []
        for r in range(1000):
            row = []
            var = QVariant('%d'%r)
            for c in range(12):
                item = QStandardItem('%s rdcd' % (time.strftime('%H"%M'%S'), r,c))
                item.setData(var)
                row.append(item)
            newRows.append(row)
        eTime = time.clock() - stTime
        outStr = 'Build f' % eTime
        format = '|%d/%b/%Y %H:%M:%S| '
        stTime = time.clock()
        self.beginRemoveRows(QModelIndex(),  0, self.rowCount())
        self.removeRows(0, self.rowCount())
        self.endRemoveRows()
        eTime = time.clock() - stTime
        outStr  = ', Remove f' % eTime
        stTime = time.clock()
        numNew = len(newRows)
        for r in range(numNew):
            self.appendRow(newRows[r])
        eTime = time.clock() - stTime
        outStr  = ', Set f' % eTime
        self.emit(SIGNAL('status'), outStr)
        #self.reset()
        return True

w = QWidget()
w.setGeometry(200,200,800,600)
hb = QVBoxLayout(w)
tv = Tree()

sb = QStatusBar()
reloader = Reloader_Thread()
tv.connect(tv, SIGNAL('status'), sb.showMessage)
reloader.connect(reloader, SIGNAL('refresh'), tv.resetModel)
reloader.connect(reloader, SIGNAL('status'), sb.showMessage)
reloader.start()

hb.addWidget(tv)
hb.addWidget(sb)
w.show()
app.setStyle('plastique')
app.processEvents(QEventLoop.AllEvents)
app.aboutToQuit.connect(reloader.quit)
app.exec_()
  

Ответ №1:

У вас правильная идея.

Вам нужен отдельный рабочий поток для повторного вычисления модели для вас (так часто, как вы хотите, на основе времени или сигнала). Затем вам нужно подключить сигнал, когда вычисление выполнено, чтобы уведомить основной поток.

Есть несколько ошибок, о которых вы должны знать.

  1. QObjects живут в потоках. Парадигма сигнала / слота (по крайней мере, в C QT) по умолчанию работает локально для потока владельца. Если вы хотите отправить сигнал по перекрестному потоку, вам нужно явно указать это (см. Документацию signal / connect).

  2. Для того, чтобы работать с моделью в рабочем потоке, вам нужно «переместить» модель в рабочий поток (должен быть метод с именем movetothread или что-то в этом роде).

  3. Убедитесь, что основной поток и рабочий поток должным образом синхронизированы.

В QT также есть QFuture (не уверен, есть ли он в PyQt), который можно использовать для хранения новой модели в главном потоке и автоматической перезагрузки ее, когда рабочий поток ее регенерирует.

Удачи.