I want to get a list of image thumbnails, When QListWidget loads a large number of image, it will become very slow.More than 200 pictures will take 5s to load. One-time loading seems to be a stupid way:
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_())
There are various possible approaches for this.
The important aspect is to delay the loading of images until it's actually required.
In the following example I used two custom roles to simplify the process: PathRole
contains the full path to the image, ImageRequestedRole
is a "flag" that tells if the image has been already loaded (or queued for loading).
The priority obviously goes to the images that are currently visible in the viewport, and we need to ensure that whenever the visible area changes the images are loaded as soon as possible.
To achieve that, I connected the scroll bar valueChanged
and rangeChanged
signals (the latter is mostly required on startup) to a function that checks the range of visible indexes and verifies whether they contain a path and if they have not been loaded nor queued yet. This will also queue loading of images whenever the window is enlarged to a bigger size which would show items previously hidden.
Once the function above finds that some images require loading, they are queued, and a timer is started (if not already active): using a timer ensures that loading is progressive and doesn't block the whole UI until all requested images are processed.
Some important aspects:
sizeHint
method and sets a decoration size even if the image is not yet loaded: this ensures that the view already reserves enough space for each item without continuously computing positions relative to every other item.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()
Finally, consider that this is a very basic and simple implementation for learning purposes:
paint
function of the delegate, otherwise some margin will always be shown on the right of the image;