Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly load images asynchronously in PyQt5?

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: enter image description here

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.

like image 699
Curtwagner1984 Avatar asked Oct 17 '22 16:10

Curtwagner1984


1 Answers

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.

like image 181
Paul Parneix Avatar answered Oct 20 '22 11:10

Paul Parneix