当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
是一个“标志”,表明是否图像已经加载(或排队等待加载)。
显然优先考虑当前在视口中可见的图像,我们需要确保每当可见区域发生变化时尽快加载图像。
为此,我将滚动条 valueChanged
和 rangeChanged
信号(后者在启动时主要需要)连接到一个检查可见索引范围并验证它们是否包含的函数一条路径,如果它们尚未加载或排队。每当 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
功能,否则右侧总是会显示一些边距图片的;
我想获取图片缩略图列表,当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
是一个“标志”,表明是否图像已经加载(或排队等待加载)。
显然优先考虑当前在视口中可见的图像,我们需要确保每当可见区域发生变化时尽快加载图像。
为此,我将滚动条 valueChanged
和 rangeChanged
信号(后者在启动时主要需要)连接到一个检查可见索引范围并验证它们是否包含的函数一条路径,如果它们尚未加载或排队。每当 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
功能,否则右侧总是会显示一些边距图片的;