处理 QListWidget 中的数千个项目并减少延迟

Handling thousands of items in a QListWidget and reducing lag

我有 QListWidget 和一本字典,我正在使用 for 循环遍历它们。 for 循环的每次迭代都会将一个项目添加到 QListWidget 中,同时为每个项目添加一些按钮和标签。一切正常,但我遇到的问题是每次刷新列表时列表都需要很长时间(1k 项大约需要 20 秒)才能加载。在此期间,GUI 完全没有响应(只要不会花费太长时间,我就可以接受)。我发现的一个解决方案是,如果我在执行迭代时隐藏 (self.hide()) QMainWindow,然后在它完成后显示 (self.show()) 它(大约 1.5 秒),刷新时间会大大减少对于 1k 项),所以我假设这是资源问题。是否有可能获得大约 1.5 秒的刷新时间,同时仍然保持 GUI 可见(并且无响应),就像冻结 GUI 这样它在刷新列表时不会占用那么多资源。

示例代码:

import sys
import time
from PyQt5.QtWidgets import QApplication, QMainWindow,  QListWidget, QListWidgetItem, QPushButton, QVBoxLayout, QWidget


class window(QMainWindow):
    def __init__(self):
        super(window, self).__init__()
        self.show()
        self.setFixedSize(800, 500)
        self.listwidget()
        self.refreshlist()

    def listwidget(self):
        self.list = QListWidget(self)
        self.list.setFixedSize(800, 500)
        self.list.show()

    def refreshlist(self): # uncomment self.hide() and self.show() to see how much faster it is
        start = time.time()
        # self.hide()
        for i in range(1000):
            item = QListWidgetItem(str(i))
            self.list.addItem(item)
            widget = QWidget(self.list)
            layout = QVBoxLayout(widget)
            layout.addWidget(QPushButton())
            self.list.setItemWidget(item, widget)
        # self.show()
        print(f"took {time.time() - start} seconds")
        """
        average time with hiding and showing was 0.3 seconds
        average time without hiding and showing was 14 seconds
        """

if __name__ == '__main__':
    app = QApplication([])
    Gui = window()
    sys.exit(app.exec_())

最初的问题是,当它显示时,每次您添加一个项目(和小部件)时,它都会重新绘制所有内容,这与隐藏它并在只有一幅画的地方显示它的任务不同。

一种不会减少加载时间但确实使 GUI 可见的替代方法是使用队列和计时器每 T 秒添加项目块。您还可以添加一个 gif,向用户指示正在加载信息。

from collections import deque
from functools import cached_property
import sys


from PyQt5.QtCore import pyqtSignal, QTimer
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QLabel,
    QListWidget,
    QListWidgetItem,
    QPushButton,
    QStackedWidget,
    QVBoxLayout,
    QWidget,
)


class ListWidget(QListWidget):
    started = pyqtSignal()
    finished = pyqtSignal()

    CHUNK = 50
    INTERVAL = 0

    def __init__(self, parent=None):
        super().__init__(parent)

        self.timer.timeout.connect(self.handle_timeout)

    @cached_property
    def queue(self):
        return deque()

    @cached_property
    def timer(self):
        return QTimer(interval=self.INTERVAL)

    def fillData(self, data):
        self.started.emit()
        self.queue.clear()
        self.queue.extend(data)
        self.timer.start()

    def handle_timeout(self):
        for i in range(self.CHUNK):
            if self.queue:
                value = self.queue.popleft()
                self.create_item(str(value))
            else:
                self.timer.stop()
                self.finished.emit()
                break

    def create_item(self, text):
        item = QListWidgetItem(text)
        self.addItem(item)
        widget = QWidget()
        layout = QVBoxLayout(widget)
        button = QPushButton(text)
        layout.addWidget(button)
        layout.setContentsMargins(0, 0, 0, 0)
        self.setItemWidget(item, widget)


class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.setFixedSize(800, 500)
        self.setCentralWidget(self.stackedWidget)
        self.stackedWidget.addWidget(self.gifLabel)
        self.stackedWidget.addWidget(self.listWidget)

        self.listWidget.started.connect(self.handle_listwidget_started)
        self.listWidget.finished.connect(self.handle_listwidget_finished)

    @cached_property
    def stackedWidget(self):
        return QStackedWidget()

    @cached_property
    def listWidget(self):
        return ListWidget()

    @cached_property
    def gifLabel(self):
        label = QLabel(scaledContents=True)
        movie = QMovie("loading.gif")
        label.setMovie(movie)
        return label

    def handle_listwidget_started(self):
        self.gifLabel.movie().start()
        self.stackedWidget.setCurrentIndex(0)

    def handle_listwidget_finished(self):
        self.gifLabel.movie().stop()
        self.stackedWidget.setCurrentIndex(1)


if __name__ == "__main__":
    app = QApplication([])

    w = Window()
    w.show()

    w.listWidget.fillData(range(1000))

    sys.exit(app.exec_())