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:
Qt Code:
  1. from PyQt5.QtCore import QAbstractListModel, Qt
  2.  
  3.  
  4. class ActorModel(QAbstractListModel):
  5. def __init__(self, db, parent=None):
  6. QAbstractListModel.__init__(self, parent)
  7. self.actors = None
  8. self.db = db
  9.  
  10. def update(self, actors):
  11. self.beginResetModel()
  12. self.actors = actors
  13. self.endResetModel()
  14.  
  15. def actor_search(self, search_string):
  16. actors = self.db.star_search("")
  17. self.update(actors)
  18.  
  19. def rowCount(self, parent=None, *args, **kwargs):
  20. return len(self.actors)
  21.  
  22. def data(self, QModelIndex, role=None):
  23. row = QModelIndex.row()
  24.  
  25. if role == Qt.UserRole + 1:
  26. return self.actors[row]["id"]
  27.  
  28. if role == Qt.DisplayRole:
  29. value = "{}".format(self.actors[row]["name"])
  30. return value
  31.  
  32. if role == Qt.UserRole:
  33. thumb_path = self.actors[row]["thumbnail"]
  34. return thumb_path
To copy to clipboard, switch view to plain text mode 

MainWindow:
Qt Code:
  1. class MainWindow(QWidget):
  2. def __init__(self):
  3. super(MainWindow, self).__init__()
  4. self.db = Database(DB_PATH)
  5. self.image_cache = {}
  6. self.image_loader = ImageLoader(self.image_cache)
  7. self.actor_model = ActorModel(self.db)
  8. self.actor_model.actor_search("")
  9. self.actor_listview_delegate = ActorListviewDelegae(self.image_cache)
  10. self.setup_ui_elements()
  11. self.connect_signals()
  12. self.image_loader.start()
  13.  
  14. def setup_ui_elements(self):
  15. self.setGeometry(100, 100, 1280, 720)
  16. self.list_view = QListView()
  17. self.list_view.setViewMode(QListView.IconMode)
  18. self.list_view.setResizeMode(QListView.Adjust)
  19. self.list_view.setModel(self.actor_model)
  20. self.list_view.setItemDelegate(self.actor_listview_delegate)
  21. self.horizontal_layout = QVBoxLayout()
  22. self.horizontal_layout.addWidget(self.list_view)
  23. self.setLayout(self.horizontal_layout)
  24.  
  25. def connect_signals(self):
  26. self.actor_listview_delegate.load_image.connect(self.load_image)
  27. self.image_loader.finished_loading.connect(self.actor_listview_delegate.paint_override)
  28.  
  29.  
  30. @pyqtSlot(int, str, QSize)
  31. def load_image(self, actor_id, path, size):
  32. self.image_loader.add_to_queue(actor_id, path, size)
To copy to clipboard, switch view to plain text mode 

ActorListviewDelegate.py:
Qt Code:
  1. class ActorListviewDelegae(QStyledItemDelegate):
  2.  
  3. def __init__(self, image_cache, parent=None):
  4. super(ActorListviewDelegae, self).__init__(parent)
  5. self.image_cache = image_cache
  6.  
  7. actor_name = QModelIndex.data(Qt.DisplayRole)
  8. actor_thumb_path = QModelIndex.data(Qt.UserRole)
  9. actor_id = QModelIndex.data(Qt.UserRole + 1)
  10. pic_rect = QRect(rect.left() - 5, rect.top() + 5, 195, 295)
  11.  
  12.  
  13. if not actor_thumb_path or not (actor_id in self.image_cache):
  14. self.load_image.emit(actor_id, actor_thumb_path, pic_rect.size())
  15. frame_color = QColor(255, 0, 0, 127)
  16. brush = QBrush(frame_color)
  17. QPainter.fillRect(pic_rect, brush) #Draws placeholder Rect
  18. else:
  19. pixmap = self.image_cache[actor_id]
  20. QPainter.drawPixmap(pic_rect, pixmap)
  21. text_rect = QRect(rect.left(), rect.top() + 300, 200, 20)
  22.  
  23. QPainter.drawText(text_rect, Qt.AlignCenter, actor_name)
  24.  
  25.  
  26. def sizeHint(self, QStyleOptionViewItem, QModelIndex):
  27. return QSize(200, 320)
To copy to clipboard, switch view to plain text mode 

ImageLoader:
Qt Code:
  1. class ImageLoader(QThread):
  2.  
  3. def __init__(self, image_cache):
  4. super(ImageLoader, self).__init__()
  5. self.image_cache = image_cache
  6. self.queue = Queue()
  7.  
  8. def load_image(self, queue_item):
  9. actor_id = queue_item[0]
  10. image_path = queue_item[1]
  11. size = queue_item[2]
  12. if not (actor_id in self.image_cache):
  13. pixmap = QPixmap(image_path).scaled(size) #Actual Image Loading
  14. self.image_cache[actor_id] = pixmap
  15.  
  16.  
  17. def run(self):
  18. while True:
  19. current_item = self.queue.get()
  20. if current_item:
  21. self.load_image(current_item)
  22. else:
  23. break
  24.  
  25. def add_to_queue(self, actor_id, image_path):
  26. item = [actor_id, image_path, size]
  27. self.queue.put(item)
To copy to clipboard, switch view to plain text mode 


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:
Qt Code:
  1. def run(self):
  2. while True:
  3. time.sleep(0.1)# <- Added delay
  4. current_item = self.queue.get()
  5. if current_item:
  6. self.load_image(current_item)
  7. else:
  8. break
To copy to clipboard, switch view to plain text mode 
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!