我必须编写一个表格视图,它应该能够处理数以万计的包含图像的单元格(每个单元格的图像都不同)。全部在 python 中使用 PySide2。
我使用线程池实现了图像的加载。问题是我必须异步通知视图已经为给定索引加载了图像,以便它可以重新加载显示。使用dataChanged 信号有效,但要处理的信号太多,并且 UI 不会显示,直到线程池处理完所有索引。
我在下面提供了一个重现问题的工作示例(无图像,仅文本)。现在,我通过让线程稍微休眠来解决这个问题(只是取消注释 Work.run 方法中的 time.sleep(1) 行),但这对我来说更像是一种肮脏的黑客攻击,而不是真正的解决方案。
我想到了以下解决方案:
- 尝试使 dataChanged 信号异步运行。我怀疑 dataChanged 和将视图更新为 AutoConnection 的任何插槽之间的默认连接。有没有办法做到这一点?
- 将修改后的索引收集在缓冲区中,并定期更新视图。我想避免这种解决方案,因为在两次评估缓冲区之间找到一个好的时间间隔是一项艰巨的任务。
您对如何避免这种阻塞行为有任何其他想法吗?
感谢您的意见!
import math
from random import choice
import string
import time
from PySide2.QtCore import QModelIndex
from PySide2.QtCore import Qt
from PySide2.QtCore import QObject
from PySide2.QtCore import Signal
from PySide2.QtCore import QThread
from PySide2.QtCore import QThreadPool
from PySide2.QtCore import QMutex
from PySide2.QtCore import QAbstractTableModel
from PySide2.QtWidgets import QTableView
class Notifier(QObject):
finished = Signal(QModelIndex, str)
class Work(QThread):
def __init__(self, *args, **kwargs):
super(Work, self).__init__(*args, **kwargs)
self.work = []
self._stopped = False
self._slept = False
def run(self):
while True:
try:
work = self.work.pop(0)
except IndexError:
work = None
if not work:
if self._slept:
break
self.msleep(500)
self._slept = True
continue
# Uncomment the following line to make the UI responsive
# time.sleep(1)
if work[0]:
c = ''.join(choice(string.ascii_uppercase + string.digits)
for _ in range(6))
work[0].finished.emit(work[1], c)
def reset(self):
self.work = []
self._stopped = True
class WorkPool(object):
def __init__(self):
self.thread_count = QThreadPool().maxThreadCount()
self.thread_pool = []
self.thread_cpt = 0
self.mutex = QMutex()
for c in range(0, self.thread_count):
self.thread_pool.append(Work())
def add_work(self, notifier, index):
new_thread = divmod(self.thread_cpt, self.thread_count)[1]
thread = self.thread_pool[new_thread]
self.thread_cpt += 1
thread.work.append((notifier, index))
if not thread.isRunning():
thread.start()
def terminate(self):
self.mutex.lock()
for t in self.thread_pool:
t.reset()
for t in self.thread_pool:
t.wait()
self.mutex.unlock()
class TableModel(QAbstractTableModel):
def __init__(self, items, *args, **kwargs):
super(TableModel, self).__init__(*args, **kwargs)
self.items = items
self.works = []
self.loader = WorkPool()
def index(self, row, column, parent=QModelIndex()):
pos = row * self.columnCount() + column
try:
return self.createIndex(row, column,self.items[pos])
except IndexError:
return QModelIndex()
def data(self, index, role):
if not index.isValid():
return None
if role == Qt.DisplayRole:
return index.internalPointer()
def columnCount(self, parent=QModelIndex()):
return 10
def rowCount(self, parent=QModelIndex()):
return int(math.ceil(float(len(self.items)) / self.columnCount()))
def refresh_content(self):
# Launch a thread to update the content of each index
for r in range(0, self.rowCount()):
for c in range(0, self.columnCount()):
index = self.index(r, c)
notifier = Notifier()
notifier.finished.connect(self.setData)
self.loader.add_work(notifier, index)
def setData(self, index, value):
if not index.isValid():
return False
self.items[index.row() * self.columnCount() + index.column()] = value
self.dataChanged.emit(index, index)
return True
class TableView(QTableView):
def closeEvent(self, *args, **kwargs):
self.model().loader.terminate()
super(TableView, self).closeEvent(*args, **kwargs)
if __name__ == '__main__':
from PySide2.QtWidgets import QApplication
app = QApplication([])
tv = TableView()
model = TableModel([None] * 99999)
tv.setModel(model)
model.refresh_content()
tv.show()
app.exec_()