如果从线程启动,QMovie 不会启动

QMovie does not start if started from a thread

各位开发者!我有一个关于 Qt 和多线程的问题。

=== 简短版本 ======================================== ===

Qt 可以做我想做的事吗?即(1)显示一个装载机; (2) 在后台下载一个gif; (3) 下载后在主界面显示下载的gifwindow?

=== 长版 ======================================== ====

我有这样的想法,当我按下一个按钮时,它:

  1. 显示加载程序;
  2. 激活从网络下载 gif 的线程;
  3. 将隐藏在主要 window 中的默认 gif 替换为下载的 gif 并显示它
  4. 隐藏加载器;

我遇到的问题是,当显示下载的 gif 时,它“冻结”或仅显示第一帧。除此之外一切都很好。但是隐藏loader后需要gif动图。

提到

So the Qt event loop is responsible for executing your code in response to various things that happen in your program, but while it is executing your code, it can't do anything else.

我相信这是问题的核心。还建议

It is recommended to use a QThread with Qt rather than a Python thread, but a Python thread will work fine if you don't ever need to communicate back to the main thread from your function.

由于我的线程替换了默认 gif 的内容,我相信它确实会回传 :(

请在下面找到我的代码:)

import sys
from time import sleep

from PyQt5.QtCore import QThread
from PyQt5.QtGui import QMovie
from PyQt5.QtWidgets import QApplication, QWidget, QPushButton, QLabel


class ChangeGif(QThread):
    """
    A worker that:
    1 - waits for 1 second;
    2 - changes the content of the default gif;
    3 - shows the default gif with new contents and hide the loader.
    """

    def __init__(self, all_widgets):
        QThread.__init__(self)
        self.all = all_widgets

    def run(self):
        sleep(1)
        self.new_gif_file = QMovie("files/new.gif")  # This is the "right" gif that freezes :(
        self.new_gif_file.start()
        self.all.gif_label.setMovie(self.new_gif_file)
        self.all.gif_label.show()
        self.all.loader_label.hide()


class MainWindow(QWidget):
    """
    Main window that shows a button. If you push the button, the following happens:
    1 - a loader is shown;
    2 - a thread in background is started;
    3 - the thread changes the contents of the default gif;
    4 - when the gif is replaced, the loader disappears and the default gif with the new content is shown
    """
    def __init__(self):
        super(MainWindow, self).__init__()

        # BUTTON
        self.button = QPushButton("Push me", self)
        self.button.setFixedSize(100, 50)
        self.button.clicked.connect(lambda: self.change_gif())

        # DEFAULT GIF
        self.gif_file = QMovie("files/default.gif")  # This is the "wrong" gif
        self.gif_file.start()
        self.gif_label = QLabel(self)
        self.gif_label.setMovie(self.gif_file)
        self.gif_label.move(self.button.width(), 0)
        self.gif_label.hide()

        # LOADER
        self.loader_file = QMovie("files/loader.gif")
        self.loader_file.start()
        self.loader_label = QLabel(self)
        self.loader_label.setMovie(self.loader_file)
        self.loader_label.move(self.button.width(), 0)
        self.loader_label.hide()

        # WINDOW SETTINGS
        self.setFixedSize(500, 500)
        self.show()

    def change_gif(self):
        self.loader_label.show()
        self.worker = ChangeGif(self)
        self.worker.start()


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

使用 Qt 的主要规则是只有一个主线程负责操作 UI 小部件。它通常被称为 GUI thread。你永远不应该尝试从另一个线程访问小部件。例如,Qt 计时器不会从另一个线程开始激活,Qt 会在运行时控制台中打印警告。在您的情况下 - 如果您将 QMovie 的所有操作放在 GUI 线程中,很可能一切都会按预期工作。

怎么办?使用信号和槽 - 它们也被设计为在线程之间工作。

您的代码应该做什么:

  1. 从主线程显示加载程序。
  2. 激活从网络下载 gif 的线程。
  3. 下载准备就绪后 - 发出信号并在连接信号和插槽时在 main GUI thread'. Remember to use Qt::QueuedConnection` 中捕获它,尽管在某些情况下会自动使用它。
  4. 在接收槽中用下载的gif替换main window中的默认gif并显示它并隐藏加载器。

您必须使用某种同步机制来避免 data-racing。互斥量就足够了。或者您可以将数据作为 signal-slot 参数传递,但如果是 gif,它可能会太大。

除了不应在主 Qt 线程之外访问或创建 UI 元素这一事实外,这对于使用在其他线程中创建的对象的 UI 元素也有效。

在您的特定情况下,这不仅意味着您不能在单独的线程中设置电影,而且您也不能在那里创建 QMovie。

在下面的示例中,我打开了一个本地文件,并使用信号将数据发送到主线程。从那里,我创建了一个 QBuffer 来将数据存储在 QMovie 可以使用的 IO 设备中。请注意,电影缓冲区 都必须具有持久引用,否则它们将在函数 returns.

后立即被垃圾回收
from PyQt5.QtCore import QThread, QByteArray, QBuffer

class ChangeGif(QThread):
    dataLoaded = pyqtSignal(QByteArray)
    def __init__(self, all_widgets):
        QThread.__init__(self)
        self.all = all_widgets

    def run(self):
        sleep(1)
        f = QFile('new.gif')
        f.open(f.ReadOnly)
        self.dataLoaded.emit(f.readAll())
        f.close()


class MainWindow(QWidget):
    # ...
    def change_gif(self):
        self.loader_label.show()
        self.worker = ChangeGif(self)
        self.worker.dataLoaded.connect(self.applyNewGif)
        self.worker.start()

    def applyNewGif(self, data):
        # a persistent reference must be kept for both the buffer and the movie, 
        # otherwise they will be garbage collected, causing the program to 
        # freeze or crash
        self.buffer = QBuffer()
        self.buffer.setData(data)
        self.newGif = QMovie()
        self.newGif.setCacheMode(self.newGif.CacheAll)
        self.newGif.setDevice(self.buffer)
        self.gif_label.setMovie(self.newGif)
        self.newGif.start()
        self.gif_label.show()
        self.loader_label.hide()

请注意,上面的示例仅用于说明目的,因为可以使用 QtNetwork 模块完成下载过程,这些模块异步工作并提供简单的信号和插槽来下载远程数据:

from PyQt5.QtCore import QUrl
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest

class MainWindow(QWidget):
    def __init__(self):
        # ...
        self.downloader = QNetworkAccessManager()

    def change_gif(self):
        self.loader_label.show()
        url = QUrl('https://path.to/animation.gif')
        self.device = self.downloader.get(QNetworkRequest(url))
        self.device.finished.connect(self.applyNewGif)

    def applyNewGif(self):
        self.loader_label.hide()
        self.newGif = QMovie()
        self.newGif.setDevice(self.device)
        self.gif_label.setMovie(self.newGif)
        self.newGif.start()
        self.gif_label.show()