QMovie не запускается, если запущен из потока
Коллеги -разработчики! У меня вопрос по Qt и многопоточности.
=== КРАТКАЯ ВЕРСИЯ ===========================================
Можно ли с Qt делать то, что я хочу? То есть (1) показать загрузчик; (2) скачать гифку в фоновом режиме; (3) показывать загруженный gif в главном окне после его загрузки?
=== ДЛИННАЯ ВЕРСИЯ ============================================
У меня есть идея, что когда я нажимаю кнопку, она:
- показывает загрузчик;
- активирует поток, загружающий гифку из сети;
- заменяет гифку по умолчанию, скрытую в главном окне, на загруженную и показывает ее
- скрывает загрузчик;
Проблема, с которой я столкнулся, заключается в том, что когда загруженный gif отображается, он "заморожен" или отображается только первый кадр. В остальном все в порядке. Однако после того, как загрузчик скрыт, необходимо, чтобы гифка была анимирована.
Он упоминается здесь, что
Таким образом, цикл событий Qt отвечает за выполнение вашего кода в ответ на различные вещи, которые происходят в вашей программе, но пока он выполняет ваш код, он не может делать ничего другого.
Я считаю, что в этом суть проблемы. Также предлагается, чтобы
Рекомендуется использовать QThread с Qt, а не поток 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_()
2 ответа
Помимо того факта, что к элементам пользовательского интерфейса нельзя обращаться или создавать их вне основного потока Qt, это справедливо также для элементов пользовательского интерфейса, которые используют объекты, созданные в других потоках.
В вашем конкретном случае это не только означает, что вы не можете установить фильм в отдельном потоке, но и не можете создать там QMovie.
В следующем примере я открываю локальный файл и использую сигнал для отправки данных в основной поток. Оттуда я создаю QBuffer для хранения данных в устройстве ввода-вывода, которое может использоваться QMovie. Обратите внимание, что и буфер, и фильм должны иметь постоянную ссылку, иначе они будут собраны мусором, как только функция вернется.
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 заключается в том, что только один основной поток отвечает за управление виджетами пользовательского интерфейса. Его часто называют
GUI thread
. Вы никогда не должны пытаться получить доступ к виджетам из других потоков. Например, таймеры Qt не будут запускаться из другого потока, а Qt выведет предупреждение в консоли времени выполнения. В вашем случае - если вы поместите все манипуляции с QMovie в поток графического интерфейса, скорее всего, все будет работать так, как ожидалось.
Что делать? Используйте сигналы и слоты - они также предназначены для работы между потоками.
Что должен делать ваш код:
- Показать загрузчик из основного потока.
- Активируйте ветку, загружающую гифку из Интернета.
- После того, как загрузка будет готова - подать сигнал и зафиксировать его в основной
GUI thread'. Remember to use
Qt::QueuedConnection` при соединении сигнала и слота, хотя в некоторых случаях он будет использоваться автоматически. - В слоте приема замените гифку по умолчанию в главном окне на загруженную, покажите ее и скройте загрузчик.
Вам придется использовать какой-то механизм синхронизации, чтобы избежать скачков данных. Мьютекса будет достаточно. Или вы можете передать данные в качестве параметра сигнального слота, но в случае гифки он, вероятно, будет слишком большим.