PDA

View Full Version : PyQt5 - Loading images asynchronously in ItemListView



curtwagner1984
4th April 2017, 18:29
Greetings Qt Center!
I'm having trouble figuring out how to load images asynchronously in QListItemView. I have a database of actors where each row in the database has the following information [id,actor_name,actor_thumb_path].
I've made a model that for test purpeses loads all the actors in the database, and a QListItemView with the setViewMode set to QListView.IconMode to display the actors names and thumbnails.
Obviously doing this in a single thread hangs the UI therefore My goal is to draw a placeholder image in the view while another thread loads the image from the disk. When the image is loaded the main thread is notified and redraws the correct image in stead of the placeholder.

To that end I created a custom delegate that receive a cache dict(), when the time comes to draw an image it checks if the actor id is present in the cache, if it is, it draws the image, if not it emits a signal sending the actor_id and the image path to the 'image_loader' thread which loads the image and places it in the common cache.

While all of this seems to work, to the extent that the images are drawn correctly, there is no performance benefit to this method as the UI still hangs when the loader thread is loading the images.


ActorModel.py:

from PyQt5.QtCore import QAbstractListModel, Qt


class ActorModel(QAbstractListModel):
def __init__(self, db, parent=None):
QAbstractListModel.__init__(self, parent)
self.actors = None
self.db = db

def update(self, actors):
self.beginResetModel()
self.actors = actors
self.endResetModel()

def actor_search(self, search_string):
actors = self.db.star_search("")
self.update(actors)

def rowCount(self, parent=None, *args, **kwargs):
return len(self.actors)

def data(self, QModelIndex, role=None):
row = QModelIndex.row()

if role == Qt.UserRole + 1:
return self.actors[row]["id"]

if role == Qt.DisplayRole:
value = "{}".format(self.actors[row]["name"])
return value

if role == Qt.UserRole:
thumb_path = self.actors[row]["thumbnail"]
return thumb_path


MainWindow:

class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.db = Database(DB_PATH)
self.image_cache = {}
self.image_loader = ImageLoader(self.image_cache)
self.actor_model = ActorModel(self.db)
self.actor_model.actor_search("")
self.actor_listview_delegate = ActorListviewDelegae(self.image_cache)
self.setup_ui_elements()
self.connect_signals()
self.image_loader.start()

def setup_ui_elements(self):
self.setGeometry(100, 100, 1280, 720)
self.list_view = QListView()
self.list_view.setViewMode(QListView.IconMode)
self.list_view.setResizeMode(QListView.Adjust)
self.list_view.setModel(self.actor_model)
self.list_view.setItemDelegate(self.actor_listview _delegate)
self.horizontal_layout = QVBoxLayout()
self.horizontal_layout.addWidget(self.list_view)
self.setLayout(self.horizontal_layout)

def connect_signals(self):
self.actor_listview_delegate.load_image.connect(se lf.load_image)
self.image_loader.finished_loading.connect(self.ac tor_listview_delegate.paint_override)


@pyqtSlot(int, str, QSize)
def load_image(self, actor_id, path, size):
self.image_loader.add_to_queue(actor_id, path, size)

ActorListviewDelegate.py:


class ActorListviewDelegae(QStyledItemDelegate):

def __init__(self, image_cache, parent=None):
super(ActorListviewDelegae, self).__init__(parent)
self.image_cache = image_cache

def paint(self, QPainter, QStyleOptionViewItem, QModelIndex):
rect = QStyleOptionViewItem.rect
actor_name = QModelIndex.data(Qt.DisplayRole)
actor_thumb_path = QModelIndex.data(Qt.UserRole)
actor_id = QModelIndex.data(Qt.UserRole + 1)
pic_rect = QRect(rect.left() - 5, rect.top() + 5, 195, 295)


if not actor_thumb_path or not (actor_id in self.image_cache):
self.load_image.emit(actor_id, actor_thumb_path, pic_rect.size())
frame_color = QColor(255, 0, 0, 127)
brush = QBrush(frame_color)
QPainter.fillRect(pic_rect, brush) #Draws placeholder Rect
else:
pixmap = self.image_cache[actor_id]
QPainter.drawPixmap(pic_rect, pixmap)
text_rect = QRect(rect.left(), rect.top() + 300, 200, 20)

QPainter.drawText(text_rect, Qt.AlignCenter, actor_name)


def sizeHint(self, QStyleOptionViewItem, QModelIndex):
return QSize(200, 320)

ImageLoader:


class ImageLoader(QThread):

def __init__(self, image_cache):
super(ImageLoader, self).__init__()
self.image_cache = image_cache
self.queue = Queue()

def load_image(self, queue_item):
actor_id = queue_item[0]
image_path = queue_item[1]
size = queue_item[2]
if not (actor_id in self.image_cache):
pixmap = QPixmap(image_path).scaled(size) #Actual Image Loading
self.image_cache[actor_id] = pixmap


def run(self):
while True:
current_item = self.queue.get()
if current_item:
self.load_image(current_item)
else:
break

def add_to_queue(self, actor_id, image_path):
item = [actor_id, image_path, size]
self.queue.put(item)


As I said before, my expectation when running this is that the UI thread would be completely free and would scroll smoothly through the list, however it is not the case. The UI hangs in exactly the same manner as when I was loading the image s in the UI thread.

A thing to note is when I'm adding a small delay in the loader's thread 'run' function as follows:

def run(self):
while True:
time.sleep(0.1)# <- Added delay
current_item = self.queue.get()
if current_item:
self.load_image(current_item)
else:
break
The UI becomes more responsive. The UI responsiveness is in direct ration to the delay, when the delay is 1 second the UI is completely responsive. (Obviously this is bad because it only loads 1 image per second )

I would really be happy if someone would point out to what am I doing wrong, or how am I to achieve the behavior I want.
Thank You!

yao
12th November 2019, 06:46
do anyone know the solution, I have the same problem