1

我必须编写一个表格视图,它应该能够处理数以万计的包含图像的单元格(每个单元格的图像都不同)。全部在 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_()
4

0 回答 0