Reputation: 33
fellow developers! I have a question regarding Qt and multithreading.
=== SHORT VERSION ===========================================
Is it possible to do what I want with Qt? That is (1) show a loader; (2) download a gif in the background; (3) show the downloaded gif in the main window after it has been downloaded?
=== LONG VERSION ============================================
I have this idea that when I push a button, it:
The problem that I'm experiencing is that when the downloaded gif is show, it is "frozen" or just the first frame is shown. Other than that everything is fine. However, it need the gif to be animated after the loader is hidden.
It is mentioned here that
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.
I believe this is the heart of the problem. It is also suggested that
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.
Since my thread replaced the contents of the default gif, I believe that it does communicates back :(
Please find my code below :)
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_()
Upvotes: 1
Views: 1736
Reputation: 48424
Other than the fact that no UI elements should be ever accessed or created outside the main Qt thread, this is valid also for UI elements that use objects created in other threads.
In your specific case, this not only means that you cannot set the movie in the separate thread, but you cannot create the QMovie there either.
In the following example, I'm opening a local file, and use a signal to send the data to the main thread. From there, I create a QBuffer to store the data in a IO device that can be used by QMovie. Note that both the buffer and the movie must have a persistent reference, otherwise they will be garbage collected as soon as the function 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()
Note that the example above is just for explanation purposes, as the downloading process can be done using QtNetwork modules, which work asynchronously and provide simple signals and slots to download remote data:
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()
Upvotes: 2
Reputation: 1941
The main rule working with Qt is that only one main thread is responsible for manipulating UI widgets. It is often referred to as GUI thread
. You should never try to access widgets from another threads. For example, Qt timers won't start activated from another thread and Qt would print warning in the runtime console. In your case - if you put all the manipulation with the QMovie in the GUI Thread, most probably everything will work as expected.
What to do? Use the signals and slots - they are also designed to work between threads.
What your code should do:
GUI thread'. Remember to use
Qt::QueuedConnection` when connecting the signal and the slot, though it will be used automatically in some cases.You'll have to use some synchronization mechanism to avoid data-racing. A mutex will be enough. Or you could pass the data as the signal-slot parameter, but in case of a gif it probably would be too big.
Upvotes: 1