Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
671 views
in Technique[技术] by (71.8m points)

pyqt5 - PyQt loading images from URL to QPixmap causes freezes and crash

I made an app that collects URLs of images from a website and displays them on display one by one. However, when you scroll through images with QComboBox, the program freezes for 2-3 seconds which is annoying, considering there are more than 100 URLs to scroll through. If you try scrolling fast the app crashes. The pictures are not even that big (less than 300KB), internet connection is good enough. Is there any solution to fix this?

Here's a snippet of the code:

from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import urllib.request
import sys

class MainWindow(QMainWindow):

    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.setWindowTitle(" ")
        lo2 = QVBoxLayout()




        self.pixmap = QPixmap()

        self.widgetIMG = QLabel()
        self.widgetIMG.setAlignment(Qt.AlignHCenter)

        self.widgetList = QComboBox()
        self.widgetList.addItems(['first image','second image', 'third image'])
        self.widgetList.currentIndexChanged.connect(self.Display)
        self.url_list = ['http://cards.hearthcards.net/3062325e.png', 'http://cards.hearthcards.net/4fc517c5.png', 'http://cards.hearthcards.net/6c9f07e2.png']


        lo2.addWidget(self.widgetIMG)
        lo2.addWidget(self.widgetList)
        widget = QWidget()             
        widget.setLayout(lo2)  
        self.setCentralWidget(widget)

    def Display(self, id):
        print(id)
        URL = self.url_list[id]
        img = urllib.request.urlopen(URL).read()

        self.pixmap.loadFromData(img)
        self.widgetIMG.setPixmap(self.pixmap.scaledToHeight(380))


if __name__ == '__main__':

    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

Maybe there is a way to preload all images on start and cache those temporarily somehow? What would be the most optimal solution?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

The problem is that the urllib.request.urlopen() function is blocking causing the window to freeze, the solution is to execute that task in another thread and send the information through signals to the main thread.

from functools import partial
import sys
import urllib.request


from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QThread, QTimer
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
    QApplication,
    QComboBox,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class Downloader(QObject):
    resultsChanged = pyqtSignal(bytes)

    @pyqtSlot(str)
    def download(self, url):
        img = urllib.request.urlopen(url).read()
        self.resultsChanged.emit(img)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("")

        self.thread = QThread(self)
        self.thread.start()

        self.downloader = Downloader()
        self.downloader.moveToThread(self.thread)
        self.downloader.resultsChanged.connect(self.on_resultsChanged)

        self.widgetIMG = QLabel(alignment=Qt.AlignHCenter)

        self.widgetList = QComboBox()
        self.widgetList.currentIndexChanged.connect(self.display)
        for text, url in zip(
            ("first image", "second image", "third image"),
            (
                "http://cards.hearthcards.net/3062325e.png",
                "http://cards.hearthcards.net/4fc517c5.png",
                "http://cards.hearthcards.net/6c9f07e2.png",
            ),
        ):
            self.widgetList.addItem(text, url)

        widget = QWidget()
        lo2 = QVBoxLayout(widget)
        lo2.addWidget(self.widgetIMG)
        lo2.addWidget(self.widgetList)
        self.setCentralWidget(widget)

    @pyqtSlot(int)
    def display(self, ix):
        url = self.widgetList.itemData(ix)
        wrapper = partial(self.downloader.download, url)
        QTimer.singleShot(0, wrapper)

    @pyqtSlot(bytes)
    def on_resultsChanged(self, img):
        pixmap = QPixmap()
        pixmap.loadFromData(img)
        self.widgetIMG.setPixmap(pixmap.scaledToHeight(380))

    def closeEvent(self, event):
        self.thread.quit()
        self.thread.wait()
        super().closeEvent(event)


if __name__ == "__main__":

    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

Another possible solution is to use Qt Network:

import sys


from PyQt5.QtCore import pyqtSlot, Qt, QUrl
from PyQt5.QtGui import QPixmap
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkReply, QNetworkRequest
from PyQt5.QtWidgets import (
    QApplication,
    QComboBox,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setWindowTitle("")

        self.manager = QNetworkAccessManager()
        self.manager.finished.connect(self.on_finished)

        self.widgetIMG = QLabel(alignment=Qt.AlignHCenter)

        self.widgetList = QComboBox()
        self.widgetList.currentIndexChanged.connect(self.display)
        for text, url in zip(
            ("first image", "second image", "third image"),
            (
                "http://cards.hearthcards.net/3062325e.png",
                "http://cards.hearthcards.net/4fc517c5.png",
                "http://cards.hearthcards.net/6c9f07e2.png",
            ),
        ):
            self.widgetList.addItem(text, url)

        widget = QWidget()
        lo2 = QVBoxLayout(widget)
        lo2.addWidget(self.widgetIMG)
        lo2.addWidget(self.widgetList)
        self.setCentralWidget(widget)

    @pyqtSlot(int)
    def display(self, ix):
        url = self.widgetList.itemData(ix)
        self.start_request(url)

    def start_request(self, url):
        request = QNetworkRequest(QUrl(url))
        self.manager.get(request)

    @pyqtSlot(QNetworkReply)
    def on_finished(self, reply):
        target = reply.attribute(QNetworkRequest.RedirectionTargetAttribute)
        if reply.error():
            print("error: {}".format(reply.errorString()))
            return
        elif target:
            newUrl = reply.url().resolved(target)
            self.start_request(newUrl)
            return
        pixmap = QPixmap()
        pixmap.loadFromData(reply.readAll())
        self.widgetIMG.setPixmap(pixmap.scaledToHeight(380))


if __name__ == "__main__":

    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

In general I prefer the second solution(The Qt-style solution) since it avoids using threads by removing complexity from the application since Qt Network uses the event loop and does not block it.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...