Search code examples
pythonmultithreadingpyqtpyqt6

PyQt6 help speeding up download and display of multiple images


I'm using PyQt6 to do an app. In a few words what i'm trying to achieve is to, on the click of a button, display a list of QGridLayouts in a QScrollArea. This list can be of any length between 2 and 60.

Each QGridLayout will contain from 1 to 20 QLabels each displaying an image (but more usually the number of labels will be around 3-5).

Each image is an image downloaded from an url using the module requests. They are not big images, each image is approx 288x288.

The code currently runs really slow and blocks the UI.

I'm looking for a way to decrease the delay between the push of the 'Run' button and the appearance of the images on the screen.

I guess multithreading would help, but i'm lost here because I don't know much about that and don't have an idea of what the approach would be here.

Here is what i'm currently trying to do:

from typing import List

import requests
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QPixmap
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, QPushButton, QScrollArea, \
    QGridLayout, QLabel


class TestWindow(QMainWindow):

    bigger_list: List[List[str]] # = ... a list of list of urls ...

    def __init__(self):
        super().__init__()
        self.setWindowTitle("Test")
        self.setFixedWidth(1230)
        self.setFixedHeight(740)
        
        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        # Define the ScrollArea where all the grids containing the images will be placed.
        output_scroll = QScrollArea()
        output_container = QWidget()
        # Define the layout of the scrollarea
        self.output_layout = QVBoxLayout()
        output_container.setLayout(self.output_layout)
        output_container.setFixedHeight(660)
        # Set properties of scrollarea
        output_scroll.setWidget(output_container)
        output_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOn)
        output_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
        output_scroll.setWidgetResizable(True)
        self.layout.addWidget(output_scroll)
 
        # Define the button that, when clicked, will start the displaying of images
        self.button = QPushButton("Run")
        self.button.clicked.connect(self.on_click)
        self.layout.addWidget(self.button)

    def on_click(self):
        # iterate over bigger list
        for smaller_list in self.bigger_list:
            # for each smaller list create a grid
            grid = QGridLayout()

            for i, url in enumerate(smaller_list):
                # iterate over smaller list elements and create image labels
                image_label = QLabel()
                # Then download the image at the urls and set it as the pixmap of the label
                response = requests.get(url)
                pixmap = QPixmap()
                pixmap.loadFromData(response.content)
                pixmap = pixmap.scaledToHeight(100)
                image_label.setPixmap(pixmap)

                grid.addWidget(image_label, int(i / 5), i % 5)
            self.output_layout.addWidget(grid)
            self.output_layout.addStretch()

def main():
    app = QApplication([])
    window = TestWindow()
    window.show()
    app.exec()


if __name__ == '__main__':
    main()

Any help would be appreciated


Solution

  • Hope this solution helps you. To use multithread in pyqt, first you have to create those labels first with a default image in the main thread (NOTE: You have to create ui in main thread). After created successfully, you can run a multithread to fetch request url images and set back to the created labels.

    from threading import Thread
    from urllib.request import urlopen
    
    def on_click(self):
        default_pixmap = self.__create_default_pixmap()
    
        # Create a list which contains labels and its request url.
        widget_and_url_list: list[tuple[QLabel, str]] = []
    
        for smaller_list in self.bigger_list:
            grid = QGridLayout()
    
    
            for i, url in enumerate(smaller_list):
                # Loop over to create widgets.
                # Keep in mind that we should create widgets in main thread. Otherwise, some errors might occur
                image_label = QLabel()
                image_label.setPixmap(default_pixmap)
                grid.addWidget(image_label, int(i / 5), i % 5)
    
                widget_and_url_list.append((image_label, url))
            self.output_layout.addLayout(grid)
            self.output_layout.addStretch()
    
        # This is how you can call a thread in python. There is a class name QThread but it is quite overkill in this case.
        # You just have to pass the function into lambda
        thread = Thread(target=lambda: self.__lazy_load_pixmaps(widget_and_url_list))
        thread.start()
    
    def __create_default_pixmap(self) -> QPixmap:
        url = 'https://upload.wikimedia.org/wikipedia/en/thumb/3/30/Java_programming_language_logo.svg/121px-Java_programming_language_logo.svg.png'
        data = urlopen(url).read()
        default_pixmap: QPixmap = QPixmap()
        default_pixmap.loadFromData(data)
        return default_pixmap.scaledToHeight(100)
    
    def __lazy_load_pixmaps(self, widget_and_url_list: list[tuple[QLabel, str]]) -> None:
        for widget_and_url in widget_and_url_list:
            widget, url = widget_and_url
            response = requests.get(url)
    
            pixmap = QPixmap()
            pixmap.loadFromData(response.content)
            pixmap = pixmap.scaledToHeight(100)
            widget.setPixmap(pixmap)
            time.sleep(0.02) # This make the loading pixmaps feel more smoothly
    
    

    Also, I see you used wrong method in this line:

    self.output_layout.addWidget(grid)
    

    Should be changed to:

    self.output_layout.addLayout(grid)