当QListWidget加载大量图片时,会变得很慢。有没有办法通过QScrollBar加载?或者,还有更好的方法?

When QListWidget loads a large number of image, it will become very slow. Is there a way to load by QScrollBar? Or is there a better way?

我想获取图片缩略图列表,当QListWidget加载大量图片时,会变得非常slow.More,200张图片加载需要5s。 一次性加载似乎是一种愚蠢的方式:

from PyQt5 import QtWidgets
from PyQt5 import QtGui
from PyQt5 import QtCore
from PyQt5 import Qt
import os

class MyQListWidgetItem(QtWidgets.QListWidgetItem):
    '''icon item'''
    def __init__(self, path, parent=None):
        self.icon = QtGui.QIcon(path)
        super(MyQListWidgetItem, self).__init__(self.icon, '', parent)

class MyQListWidget(QtWidgets.QListWidget):
    def __init__(self):
        super(MyQListWidget, self).__init__()
        path = './imgpath'
        self.setFlow(QtWidgets.QListView.LeftToRight)
        self.setIconSize(QtCore.QSize(180, 160))
        self.setResizeMode(Qt.QListWidget.Adjust)

        #add icon
        for fp in os.listdir(path):
            self.addItem(MyQListWidgetItem(os.path.join(path, fp), self))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MyQListWidget()
    w.show()
    sys.exit(app.exec_())

对此有多种可能的方法。

重要的方面是延迟图像的加载,直到实际需要它。

在下面的示例中,我使用了两个自定义 roles 来简化过程:PathRole 包含图像的完整路径,ImageRequestedRole 是一个“标志”,表明是否图像已经加载(或排队等待加载)。

显然优先考虑当前在视口中可见的图像,我们需要确保每当可见区域发生变化时尽快加载图像。

为此,我将滚动条 valueChangedrangeChanged 信号(后者在启动时主要需要)连接到一个检查可见索引范围并验证它们是否包含的函数一条路径,如果它们尚未加载或排队。每当 window 放大到更大的尺寸时,这也会排队加载图像,这会显示以前隐藏的项目。

一旦上述函数发现某些图像需要加载,它们就会排队,并启动一个计时器(如果尚未激活):使用计时器可确保加载是渐进的在处理完所有请求的图像之前,不会阻止整个 UI。

一些重要方面:

  • 图像存储为它们的来源(否则您很容易耗尽所有资源),但按比例缩小了。
  • “延迟加载器”确保当前队列完成后立即延迟加载当前未显示的图像;请注意,如果您打算浏览 大量 也非常大的图像,则不建议这样做。
  • 由于图片不是立即加载的,默认情况下项目没有正确的大小:设置图标大小是不够的,因为直到项目实际有“装饰”才考虑该大小;为了解决这个问题,使用了一个委托,它实现 sizeHint 方法并设置装饰尺寸,即使图像尚未加载:这确保视图已经为每个项目预留了足够的 space连续计算相对于其他项目的位置。
  • 设置“已加载”标志需要在模型上写入数据,默认情况下会导致视图再次计算尺寸;为了避免这种情况,使用了一个临时的信号拦截器,这样模型就可以在不让视图知道的情况下更新。
  • 出于性能原因,您不能根据图片的纵横比为每张图片设置不同的宽度。
PathRole = QtCore.Qt.UserRole + 1
ImageRequestedRole = PathRole + 1


class ImageDelegate(QtWidgets.QStyledItemDelegate):
    def initStyleOption(self, opt, index):
        super().initStyleOption(opt, index)
        if index.data(PathRole):
            opt.features |= opt.HasDecoration
            opt.decorationSize = QtCore.QSize(180, 160)


class MyQListWidget(QtWidgets.QListWidget):
    def __init__(self):
        super(MyQListWidget, self).__init__()
        path = './imgpath'
        self.setFlow(QtWidgets.QListView.LeftToRight)
        self.setIconSize(QtCore.QSize(180, 160))
        self.setResizeMode(Qt.QListWidget.Adjust)

        for fp in os.listdir(path):
            imagePath = os.path.join(path, fp)
            item = QtWidgets.QListWidgetItem()
            if os.path.isfile(imagePath):
                item.setData(PathRole, imagePath)
            self.addItem(item)

        self.imageDelegate = ImageDelegate(self)
        self.setItemDelegate(self.imageDelegate)
        self.imageQueue = []
        self.loadTimer = QtCore.QTimer(
            interval=25, timeout=self.loadImage, singleShot=True)
        self.lazyTimer = QtCore.QTimer(
            interval=100, timeout=self.lazyLoadImage, singleShot=True)
        self.lazyIndex = 0

        self.horizontalScrollBar().valueChanged.connect(self.checkVisible)
        self.horizontalScrollBar().rangeChanged.connect(self.checkVisible)

    def checkVisible(self):
        start = self.indexAt(QtCore.QPoint()).row()
        end = self.indexAt(self.viewport().rect().bottomRight()).row()
        if end < 0:
            end = start
        model = self.model()
        for row in range(start, end + 1):
            index = model.index(row, 0)
            if not index.data(ImageRequestedRole) and index.data(PathRole):
                with QtCore.QSignalBlocker(model):
                    model.setData(index, True, ImageRequestedRole)
                self.imageQueue.append(index)
        if self.imageQueue and not self.loadTimer.isActive():
            self.loadTimer.start()

    def requestImage(self, index):
        with QtCore.QSignalBlocker(self.model()):
            self.model().setData(index, True, ImageRequestedRole)
        self.imageQueue.append(index)
        if not self.loadTimer.isActive():
            self.loadTimer.start()

    def loadImage(self):
        if not self.imageQueue:
            return
        index = self.imageQueue.pop()
        image = QtGui.QPixmap(index.data(PathRole))
        if not image.isNull():
            self.model().setData(
                index, 
                image.scaled(self.iconSize(), QtCore.Qt.KeepAspectRatio), 
                QtCore.Qt.DecorationRole
            )
        if self.imageQueue:
            self.loadTimer.start()
        else:
            self.lazyTimer.start()

    def lazyLoadImage(self):
        self.lazyIndex += 1
        if self.lazyIndex >= self.count():
            return
        index = self.model().index(self.lazyIndex, 0)
        if not index.data(ImageRequestedRole) and index.data(PathRole):
            with QtCore.QSignalBlocker(self.model()):
                self.model().setData(index, True, ImageRequestedRole)
            image = QtGui.QPixmap(index.data(PathRole))
            if not image.isNull():
                self.model().setData(
                    index, 
                    image.scaled(self.iconSize(), QtCore.Qt.KeepAspectRatio), 
                    QtCore.Qt.DecorationRole
                )
        else:
            self.lazyLoadImage()
            return
        if not self.imageQueue:
            self.lazyTimer.start()

最后,考虑到这是一个非常基本和简单的实现,用于学习目的:

  • 图像查看器应该将所有图像存储在内存中(甚至不像我的示例中的缩略图):考虑图像存储为光栅(“位图”),所以即使是缩略图也可能比原始压缩图像占用更多的内存;
  • 在达到最大缩略图数量的情况下,可以在临时路径中使用缓存;
  • 图像加载应该在一个单独的线程中进行,可能会显示一个占位符,直到该过程完成;
  • 应进行适当的检查以确保文件实际上是图像,and/or 如果图像已损坏;
  • 除非您打算在缩略图旁边显示其他内容(文件名、统计信息等),否则您应该考虑实现委托的 paint 功能,否则右侧总是会显示一些边距图片的;