PyQt 将图像从 URL 加载到 QPixmap 导致冻结和崩溃

PyQt loading images from URL to QPixmap causes freezes and crash

我制作了一个应用程序,它从一个网站收集图像的 URL,并在显示器上将它们一张一张地显示出来。但是,当您使用 QComboBox 滚动浏览图像时,程序会冻结 2-3 秒,这很烦人,因为要滚动浏览的 URL 超过 100 个。如果您尝试快速滚动,应用程序会崩溃。图片甚至没有那么大(不到 300KB),互联网连接足够好。有解决办法吗?

这是代码片段:

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_())

也许有一种方法可以在启动时预加载所有图像并以某种方式临时缓存这些图像?最佳解决方案是什么?

问题是 urllib.request.urlopen() 函数阻塞导致 window 冻结,解决方案是在另一个线程中执行该任务并通过信号将信息发送到主线程。

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_())

另一种可能的解决方案是使用 Qt 网络:

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_())

一般来说,我更喜欢第二种解决方案(Qt 风格的解决方案),因为它通过消除应用程序的复杂性来避免使用线程,因为 Qt 网络使用事件循环并且不会阻塞它。