Stuart Buckingham
Stuart Buckingham

Reputation: 1774

Creating QPixmaps in a thread

I have a problem where I am trying to load a whole lot of png images and subsequently display theme using PyQt. My current workflow is to use a multiprocessor pool to map a function which opens each file with 'rb' values, then reads the bytes of each file into a unified list. Finally the parent process then displays the image by calling the fromImageData method of a QPixmap object. This method seems to work fine, but is quite slow to redraw a new pixmap each time I switch between images (8K resolution).

I was hoping that it may be faster to instead create a pixmap for each image and cycle through the pixmap rather than recreating the same pixmap with a new image at each step. To do this, I tried to create a pixmap in the multiprocess function, however this is not allowed because there is no parent QApp in the thread.

My question is if there is a proper way to do this? I have alsp thought of doing it with celery/reddis, but i cannot see that having any different ourcome. Is creating a new pixmap for each image and switching them using setPixmap is even a viable option or are there more appropriate ways to achieve this?

Upvotes: 1

Views: 1767

Answers (1)

bnaecker
bnaecker

Reputation: 6430

You should be able to do this using a QThreadPool and some QRunnables which wrap the code that loads the pixmaps. Something like:

from PyQt5 import QtCore, QtGui

class PixmapLoader(QtCore.QRunnable):
    def __init__(self, filename):
        super().__init__()
        self.filename = filename
    def run(self):
        # Load pixmap at filename 
        # ...
        # then emit in a signal
        loaded.emit(pixmap)

    loaded = QtCore.pyqtSignal(QtGui.QPixmap)

Then somewhere in the main application, create a thread-pool, run the loading objects, and handle their signals.

pool = QtCore.QThreadPool()
loaders = [PixmapLoader(filename) for filename in filenames]
for loader in loaders:
    loader.loaded.connect(handle_new_pixmap)
    pool.start(loader)

def handle_new_pixmap(QtGui.QPixmap):
   # do stuff with pixmap

I've not tried this, but because Qt is handling the threading, this should be able to take advantage of multiple threads just fine.

EDIT

As explained in the comments, this won't work. I've forgotten that QRunnable doesn't inherit QObject, and that QPixmaps cannot be created outside the main thread. But it's pretty straightforward to instead use an image loader object, move it to one or more background threads, in which it loads a QImage, and then sends that to the main thread for later use. Here's tested code that will do the bare-bones, loading all PNG files in the current directory.

#!/usr/bin/env python3

import os

from PyQt5.QtCore import pyqtSignal, QObject, QThread
from PyQt5.QtGui import QImage
from PyQt5.QtWidgets import QApplication

class ImageLoader(QObject):
    loaded = pyqtSignal(str, QImage)

    def __init__(self, filename):
        super().__init__()
        self.filename = filename

    def on_load_signal(self):
        img = QImage(self.filename)
        self.loaded.emit(self.filename, img)   


class LoaderManager(QObject):
    request_img_load = pyqtSignal()

    def __init__(self):
        super().__init__()
        self.loaders = list(map(ImageLoader, 
                filter(lambda f: f.endswith('.png'), os.listdir())))
        self.bg_thread = QThread()

        for loader in self.loaders:
            self.request_img_load.connect(loader.on_load_signal)
            loader.loaded.connect(self.handle_img_loaded)
            loader.moveToThread(self.bg_thread)

        self.bg_thread.start()

    def __del__(self):
        self.bg_thread.quit()

    def load_all(self):
        self.request_img_load.emit()

    def handle_img_loaded(self, name, img):
        print('File {} of size {} loaded'.format(name, img.byteCount()))

if __name__ == '__main__':

    app = QApplication([])
    manager = LoaderManager()
    manager.load_all()

Upvotes: 2

Related Questions