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_cacheself.loader_thread = loader_threadself.t1.connect(self.loader_thread.insert_into_queue)def paint(self, QPainter, QStyleOptionViewItem, QModelIndex):rect = QStyleOptionViewItem.rectactor_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_imageprint("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] = pixmapprint("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] = pixmapcached_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.