问题描述
资深开发者!我对Qt和多线程有疑问。
===简短版本========================================== =
可以用Qt做我想做的事吗?即(1)显示一个装载机; (2)在后台下载gif; (3)下载后的gif在主窗口中显示吗?
===长版========================================== ==
我的想法是,当我按下按钮时,它会:
我遇到的问题是,当显示下载的gif时,它是“冻结的”或仅显示第一帧。除此之外,一切都很好。但是,隐藏加载程序后,需要对gif进行动画处理。
提到here,
我相信这是问题的核心。还建议
建议将带有Qt的QThread而不是Python线程使用,但是如果您不需要从函数返回到主线程,则Python线程可以正常工作。
由于我的线程替换了默认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元素外,这对于使用其他线程中创建的对象的UI元素也有效。
在您的特定情况下,这不仅意味着您无法在单独的线程中设置影片,而且也无法在其中创建QMovie。
在下面的示例中,我打开一个本地文件,并使用信号将数据发送到主线程。从那里,我创建一个QBuffer将数据存储在QMovie可以使用的IO设备中。请注意,缓冲区和电影都必须具有持久引用,否则函数返回后将立即对其进行垃圾回收。
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()
,
使用Qt的主要规则是只有一个主线程负责操作UI小部件。它通常称为GUI thread
。您永远不要尝试从另一个线程访问窗口小部件。例如,Qt计时器不会从其他线程开始激活,并且Qt会在运行时控制台中显示警告。就您而言-如果将所有与QMovie一起的操作都放在GUI线程中,则很可能一切都会按预期进行。
该怎么办?使用信号和插槽-它们也被设计为在线程之间工作。
您的代码应该做什么:
- 从主线程显示一个加载器。
- 激活一个从网络下载gif的线程。
- 下载完成后-连接信号和插槽时,发出信号并捕获到主
GUI thread'. Remember to use
Qt :: QueuedConnection`中,尽管在某些情况下会自动使用它。 - 在接收插槽中,将主窗口中的默认gif替换为下载的gif,然后显示它并隐藏加载程序。
您必须使用某种同步机制来避免数据争用。互斥量就足够了。或者,您可以将数据作为signal-slot参数传递,但如果是gif,则可能太大。