当 QLabel 的内容从另一个线程更新时,GIF 不动画
Posted
技术标签:
【中文标题】当 QLabel 的内容从另一个线程更新时,GIF 不动画【英文标题】:GIF does not animate when content of QLabel is updated from another thread 【发布时间】:2020-07-19 05:44:56 【问题描述】:我创建了一个简单的应用程序,它使用 PyQt5 在 QLabel 中显示图像。图片可以是静态的(例如:png、jpeg、bmp),也可以是 gif。
下面的示例代码说明如下:
类ImageDisplayer() 负责创建一个QLabel,其中包含要显示的所需图像。 update_image() 方法允许将 QLabel 中显示的图像更新为所需的新图像。 QLabel 窗口显示在所需的屏幕上(使用多台显示器时)。
main() 方法是一个 PyQt5 应用程序的简单演示,它使用 ImageDisplayer 类在 QLabel 上显示所需的图像。在现实世界的最终用例中,这个主要的 Qt 应用程序将具有其他复杂的小部件/逻辑来与用户交互(例如:询问用户要显示哪个图像),并且从 ImageDisplayer 显示的 QLabel 将始终全屏显示所需的图像在辅助监视器上。但是,为简单起见,我没有在下面的示例代码中展示这一点。
test_image_sequence() 方法是一个简单的函数,它循环遍历各种测试图像以调试/排除 ImageDisplayer() 类的开发问题。
问题: ImageDisplayer 类按预期工作,但是,当我尝试从单独的线程调用 update_image() 方法时,gif 图像没有动画。例如,当我使用 QThreadPool 在单独的线程中运行 test_image_sequence() 方法时,静态图像按预期显示,但 gif 不是动画。
import os, sys, time, pathlib
from PyQt5 import QtWidgets
from PyQt5.QtCore import Qt, QRunnable, QThreadPool
from PyQt5.QtGui import QColor, QPixmap, QMovie
from PyQt5.QtWidgets import QApplication, QLabel, QWidget
CURRENT_PATH = str(pathlib.Path(__file__).parent.absolute())
def main():
app = QtWidgets.QApplication(sys.argv)
my_image_window = ImageDisplayer(monitor_num=0,)
# Method 1: When not using threads, the gif animates as expected
# my_image_window.update_image(CURRENT_PATH + r'\test_images\gif_image_2.gif')
# Method 2: When using threads, gif does NOT animate
thread = QThreadPool.globalInstance()
worker = Worker(test_image_sequence, my_image_window)
thread.start(worker)
app.exec_()
def test_image_sequence(widget):
print('Will start testing seq. of images')
time.sleep(1)
images = []
images.append(CURRENT_PATH + r'\test_images\static_image_1.png')
images.append(CURRENT_PATH + r'\test_images\static_image_2.png')
images.append(CURRENT_PATH + r'\test_images\gif_image_1.gif')
images.append(CURRENT_PATH + r'\test_images\gif_image_2.gif')
for i in images:
print('Updating image to:', i)
widget.update_image(pattern_file=i)
time.sleep(3)
class ImageDisplayer():
def __init__(self, monitor_num=0,):
# Get instance of the current QApplication
self.app = QtWidgets.QApplication.instance() #https://***.com/a/53387775/4988010
# Gather info on avaliable monitor and select the desired one
self.screen = self.app.screens()[monitor_num]
self.screen_width = self.screen.size().width()
self.screen_height = self.screen.size().height()
# Init class attributes
self.pattern_file = None # Set a default pattern if given during init
self.pixmap = None # Static image content
self.pixmap_mv = None # Movie content
self.scale_window = 2 # For debugging: If not full screen the window will be scaled by half of screen size
# Define a constant color images when no image displayed
self.pixmap_blank = QPixmap(self.screen_width, self.screen_height)
self.pixmap_blank.fill(QColor('green'))
self.pixmap = self.pixmap_blank # Default during init
self.app_widget = None # QLabel widget object
self.setupGUI() # Setup and show the widget
def setupGUI(self):
print('Setting up the QLabel')
# Create QLabel object
self.app_widget = QLabel()
self.app_widget.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.app_widget.setStyleSheet('QLabel background-color: green;')
self.app_widget.setCursor(Qt.BlankCursor) # A blank/invisible cursor, typically used when the cursor shape needs to be hidden.
# Scale the widget to the size of the screen
self.app_widget.setGeometry(0, 0, self.screen_width/self.scale_window , self.screen_height/self.scale_window) # Set the size of Qlabel to size of the screen
# Set a default pattern during init
self.app_widget.setPixmap(self.pixmap)
self.app_widget.show()
# Move window to topleft corner of the selected screen
self.app_widget.windowHandle().setScreen(self.screen)
self.app_widget.move(self.screen.geometry().topLeft())
def update_image(self, pattern_file):
self.pattern_file = pattern_file
print('Pattern file: ', pattern_file)
filename, file_extension = os.path.splitext(pattern_file) # Get filename and extension https://***.com/a/541394/4988010
self.app_widget.clear() # Clear all existing content of the QLabel
self.pixmap = QPixmap(self.pattern_file)
if (file_extension == '.png') or (file_extension == '.jpg') or (file_extension == '.jpeg') or (file_extension == '.bmp'):
# File is a static image
# https://doc.qt.io/qt-5/qpixmap.html
print('Image is a static')
self.app_widget.setPixmap(self.pixmap)
elif (file_extension == '.gif'):
# File is a movie
print('Image is movie')
self.pixmap_mv = QMovie(self.pattern_file)
# Connect the "finished() signal to movie_finished() slot"
self.pixmap_mv.finished.connect(self.movie_finished)
# Debugging text
print('Movie is valid: ', self.pixmap_mv.isValid())
print('loopCount: ', self.pixmap_mv.loopCount())
print('frameCount: ', self.pixmap_mv.frameCount())
print('Default speed: ', self.pixmap_mv.speed())
self.app_widget.setMovie(self.pixmap_mv)
self.pixmap_mv.start()
def movie_finished(self):
print('Movie finished')
# After movie is finished, show blank screen
self.app_widget.setPixmap(self.pixmap_blank)
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
def run(self):
self.fn(*self.args, **self.kwargs)
if __name__ == "__main__":
main()
【问题讨论】:
【参考方案1】:基本 Qt 规则:您不应该直接从另一个线程修改 GUI,因为它不是线程安全的,而是使用信号。例如,在这种情况下,您可以创建一个作为代理的类:
# ...
class Signaller(QObject):
imageChanged = pyqtSignal(str)
def update_image(self, pattern_file):
self.imageChanged.emit(pattern_file)
def main():
app = QtWidgets.QApplication(sys.argv)
my_image_window = ImageDisplayer(monitor_num=0,)
signaller = Signaller()
signaller.imageChanged.connect(my_image_window.update_image)
thread = QThreadPool.globalInstance()
worker = Worker(test_image_sequence, signaller)
thread.start(worker)
app.exec_()
# ...
【讨论】:
以上是关于当 QLabel 的内容从另一个线程更新时,GIF 不动画的主要内容,如果未能解决你的问题,请参考以下文章