I created a simple app which uses PyQt5 to display an image inside a QLabel. The image can be static (ex: png, jpeg, bmp) or it can a gif.
The description of the example code below is as follows:
Class ImageDisplayer() is responsible for creating a QLabel which contains the desired image to be displayed. The update_image() method allows the shown image inside the QLabel to be updated to the desired new image. The QLabel window is shown on the desired screen (when using multiple monitors).
The main() method is an simple demo of a PyQt5 app that uses the ImageDisplayer class to display a desired image on the QLabel. In the real world end use case, this main Qt app would have other complex widgets/logic for interaction with the user (ex: asking user which image to display) and the QLabel displayed from the ImageDisplayer will always show the desired image in full screen on a secondary monitor. However, for simplicity I have not shown this in the example code below.
The test_image_sequence() method is a simple function which loops through various test images to debug/troubleshoot the development of the ImageDisplayer() class.
Problem: The ImageDisplayer class works as expected, however, gif images are not animating when I try to call the update_image() method from a separate thread. For example, when I run the test_image_sequence() method in a separate thread using QThreadPool, the static images are displayed as expected, but gif are not animated.
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://stackoverflow.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://stackoverflow.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()