当 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 不动画的主要内容,如果未能解决你的问题,请参考以下文章

pyqt QLabel 在另一个线程更新其文本时未呈现

如何从另一个线程更新 GUI?

如何从另一个线程更新 GUI?

如何从另一个线程更新 GUI?

从另一个线程更新 excel 电子表格

从另一个线程更新 oxyplot 模型