I'm trying to figure out how to accomplish an async image load correctly, in PyQt Qlistview.
My main widget consists of a Qlistview
and a QLineEdit
textbox.
I have a database of actors which I query using a subclass of QAbstractListModel
When text is entered in the textbox, the database is queried and the Model is populated with the results. Results are then displayed in the Qlistview. (A result for each Actor contains the actors name and a path to an image.)
Like so:
The problem begins when the result set is too large (Larger than 50), loading the images from disk is taking it's toll and hanging the UI. The behaviour I wish to achieve is to initially load a placeholder image for all the results and then in a different thread load the specific image from disk and when it's loaded update the Qlistview item with the newly loaded image.
To that end I created a custom QItemDelegate
class that has a cache of all images that needs to be loaded. If the image is not in the cache then it draws the placeholder image and sends a signal to a another thread that loads that image and puts it in the cache.
My Delegate class:
class MyDelegate(QStyledItemDelegate):
t1 = pyqtSignal(str, str, dict)
def __init__(self, image_cache, loader_thread, parent=None):
super(MyDelegate, self).__init__(parent)
self.placeholder_image = QPixmap(PLACEHOLDER_IMAGE_PATH).scaled(200, 300)
self.image_cache = image_cache
self.loader_thread = loader_thread
self.t1.connect(self.loader_thread.insert_into_queue)
def paint(self, QPainter, QStyleOptionViewItem, QModelIndex):
rect = QStyleOptionViewItem.rect
actor_name = QModelIndex.data(Qt.DisplayRole)
actor_thumb = QModelIndex.data(Qt.UserRole)
pic_rect = QRect(rect.left(), rect.top(), 200, 300)
text_rect = QRect(rect.left(), rect.top() + 300, 200, 20)
try:
cached_thumb = self.image_cache[actor_name]
print("Got image: {} from cache".format(actor_name)
except KeyError as e:
self.t1.emit(actor_name, actor_thumb, self.image_cache)
cached_thumb = self.placeholder_image
print("Drawing placeholder image for {}".format(actor_name)
QPainter.drawPixmap(pic_rect, cached_thumb)
QPainter.drawText(text_rect, Qt.AlignCenter, actor_name)
if QStyleOptionViewItem.state & QStyle.State_Selected:
highlight_color = QStyleOptionViewItem.palette.highlight().color()
highlight_color.setAlpha(50)
highlight_brush = QBrush(highlight_color)
QPainter.fillRect(rect, highlight_brush)
def sizeHint(self, QStyleOptionViewItem, QModelIndex):
return QSize(200, 320)
LoaderThread:
class LoaderThread(QObject):
def __init__(self):
super(LoaderThread, self).__init__()
@pyqtSlot(str, str, dict)
def insert_into_queue(self, name, thumb_path, image_cache):
print("Got signal, loading image for {} from disk".format(name))
pixmap = QPixmap(thumb_path).scaled(200, 300)
image_cache[name] = pixmap
print("Image for {} inserted to cache".format(name))
Relevant part of the main window __init__
method:
image_cache = {}
lt = loader_tread.LoaderThread()
self.thread = QThread()
lt.moveToThread(self.thread)
self.thread.start()
self.delegate = MyDelegate(image_cache, lt)
While this approach seems to work as so far as the images are loading correctly, the UI is hanging when multiple calls to self.t1.emit(actor_name, actor_thumb, self.image_cache)
in MyDelegate are made.
In fact, the delay is almost identical to when the images are loaded in the same thread like so:
try:
cached_thumb = self.image_cache[actor_name]
print("Got image: {} from cache".format(QModelIndex.data(Qt.DisplayRole)))
except KeyError as e:
# self.t1.emit(actor_name, actor_thumb, self.image_cache)
pixmap = QPixmap(actor_thumb).scaled(200,300)
self.image_cache[actor_name] = pixmap
cached_thumb = self.image_cache[actor_name]
If someone has any pointers about what I am doing wrong or about how the desired behavior can be achieved they will be well received.
P.S I'm aware that I can limit the result set in the database query, however this is not what I wish to do.
I just had the same problem today : Trying to load thumbnails QPixmaps on a separate Qthread, and it was hanging the UI.
I figured out that for some reason... using QPixmap to load image from disk will always "freeze" the main GUI Thread :
pixmap = QPixmap(thumb_path).scaled(200, 300) # Will hang UI
One solution ; load the image using a QImage object, then generate the QPixmap from image, The following code do the job for me :
image = QImage(thumb_path)
pixmap = QPixmap.fromImage(image).scaled(200, 300)
Scrolling is smooth, tested with 500+ thumbnails.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With