-1

I want to use PyQt to display a selected static image or an animated gif using QLabel widget. For my specific use case, I am only using Qt to display the image while rest of my Python code does NOT require Qt. I want my Qt app to run in the background (separate thread). This is because after running app.exec_(), the Qt app goes into a continuous event loop thus blocking the rest of the logic in my Python code.

I was able to achieve a working solution (code below) using the two classes. Using this method, I am able to update static or animated images whenever I would like and the code works as expected. However, instead of having two classes, I would like to consolidate into one class (code also below). The problem with one class approach is that the displayed gif is NOT animated. The code works as expected whenever I update static images, however, when using gif images, the displayed gif is NOT animated. Why is does the one class approach not animate the displayed gif? Why am I doing wrong?

Working: Two class approach

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
import time
import threading
from PyQt5.QtCore import QObject, pyqtSignal
import os


def main():

    print('Step 1')
    print('     Some logic here without QT')

    print('Step 2')
    print('     Launch QT app to run in background')

    myapp = myImageDisplayApp()

    print('Step 3')
    print('     Continue some logic while QT running in background')
    time.sleep(2)

    print('Step 4')
    print('     Update the displayed image in the QT app running in background')

    myapp.emit_image_update('qt_test_static.png')
    time.sleep(1)
    myapp.emit_image_update('qt_test_movie.gif')
    time.sleep(1)   

    print('Step 5')
    print('     Continue logic while QT running in background')
    time.sleep(3)

class myImageDisplayApp (QObject):

    # Define the custom signal
    # https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html#the-pyqtslot-decorator
    signal_update_image = pyqtSignal(str)

    def __init__ (self):

        super().__init__()

        self.app = QApplication(sys.argv)

        # Setup the seperate thread 
        # https://stackoverflow.com/a/37694109/4988010
        self.thread = threading.Thread(target=self.run_app_widget_in_background) 
        self.thread.daemon = True
        self.thread.start()

    def run_app_widget_in_background(self):

        self.app = QApplication(sys.argv)
        self.my_bg_qt_app = qtAppWidget(qt_patterndisplay_threader=self)
        self.app.exec_()


    def emit_image_update(self, pattern_file=None):
        print('signal_image_update')
        self.pattern_file = pattern_file
        self.signal_update_image.emit(self.pattern_file)


class qtAppWidget (QLabel):

    def __init__ (self, qt_patterndisplay_threader):

        super().__init__()

        # Connect the singal to slot
        qt_patterndisplay_threader.signal_update_image.connect(self.updateImage)

        self.app = QApplication.instance()

        # Get avaliable screens/monitors
        # https://doc.qt.io/qt-5/qscreen.html
        # Get info on selected screen 
        self.screens_available = self.app.screens()
        self.screen = self.screens_available[0]
        self.screen_width = self.screen.size().width()
        self.screen_height = self.screen.size().height()

        self.setupGUI()

    def setupGUI(self):

        # Define a constant color images
        self.pixmap = QPixmap(self.screen_width, self.screen_height)
        self.pixmap.fill(QColor('blue'))

        scale_window = 2

        # Create QLabel object
        self.app_widget = QLabel()
        self.app_widget.setGeometry(0, 0, self.screen_width/scale_window , self.screen_height/scale_window)         # Set the size of Qlabel to size of the screen
        self.app_widget.setWindowTitle('myImageDisplayApp')
        self.app_widget.setAlignment(Qt.AlignLeft | Qt.AlignTop) #https://doc.qt.io/qt-5/qt.html#AlignmentFlag-enum                         
        self.app_widget.setPixmap(self.pixmap)
        self.app_widget.show()


    def updateImage(self, pattern_file=None):
        print('\nPattern 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.pattern_file = pattern_file
        self.pixmap = QPixmap(self.pattern_file)

        if file_extension != '.gif':
            # File is a static image
            print('Image is a static')
            self.app_widget.setPixmap(self.pixmap)
        else:
            # File is a movie
            print('Image is movie')

            self.pixmap_mv = QMovie(self.pattern_file)
            print('Check to see if gif valid: ', self.pixmap_mv.isValid())
            self.app_widget.setMovie(self.pixmap_mv)
            self.pixmap_mv.start()


if __name__ == "__main__":

    main()

NOT Working: One class approach

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
import time
import threading
from PyQt5.QtCore import QObject, pyqtSignal
import os


def main():

    print('Step 1')
    print('     Some logic here without QT')

    print('Step 2')
    print('     Launch QT app to run in background')

    myapp = myImageDisplayApp()

    print('Step 3')
    print('     Continue some logic while QT running in background')
    time.sleep(2)

    print('Step 4')
    print('     Update the displayed image in the QT app running in background')

    myapp.emit_image_update('qt_test_static.png')
    time.sleep(1)
    myapp.emit_image_update('qt_test_movie.gif')
    time.sleep(1)   

    print('Step 5')
    print('     Continue logic while QT running in background')
    time.sleep(3)

class myImageDisplayApp (QObject):

    # Define the custom signal
    # https://www.riverbankcomputing.com/static/Docs/PyQt5/signals_slots.html#the-pyqtslot-decorator
    signal_update_image = pyqtSignal(str)

    def __init__ (self):

        super().__init__()

        # Connect the singal to slot
        self.signal_update_image.connect(self.updateImage)

        self.app = QApplication(sys.argv)

        # Get avaliable screens/monitors
        # https://doc.qt.io/qt-5/qscreen.html
        # Get info on selected screen 
        self.screens_available = self.app.screens()
        self.screen = self.screens_available[0]
        self.screen_width = self.screen.size().width()
        self.screen_height = self.screen.size().height()

        # Setup the seperate thread 
        # https://stackoverflow.com/a/37694109/4988010
        self.thread = threading.Thread(target=self.run_app_widget_in_background) 
        self.thread.daemon = True
        self.thread.start()

    def run_app_widget_in_background(self):

        self.app = QApplication(sys.argv)
        # self.my_bg_qt_app = qtAppWidget(qt_patterndisplay_threader=self)
        self.setupGUI()
        self.app.exec_()

    def setupGUI(self):

        # Define a constant color images
        self.pixmap = QPixmap(self.screen_width, self.screen_height)
        self.pixmap.fill(QColor('red'))

        scale_window = 2

        # Create QLabel object
        self.app_widget = QLabel()
        self.app_widget.setGeometry(0, 0, self.screen_width/scale_window , self.screen_height/scale_window)         # Set the size of Qlabel to size of the screen
        self.app_widget.setWindowTitle('myImageDisplayApp')
        self.app_widget.setAlignment(Qt.AlignLeft | Qt.AlignTop) #https://doc.qt.io/qt-5/qt.html#AlignmentFlag-enum                         
        self.app_widget.setPixmap(self.pixmap)
        self.app_widget.show()

    def emit_image_update(self, pattern_file=None):
        print('signal_image_update')
        self.pattern_file = pattern_file
        self.signal_update_image.emit(self.pattern_file)

    def updateImage(self, pattern_file=None):
        print('\nPattern 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.pattern_file = pattern_file
        self.pixmap = QPixmap(self.pattern_file)

        if file_extension != '.gif':
            # File is a static image
            print('Image is a static')
            self.app_widget.setPixmap(self.pixmap)
        else:
            # File is a movie
            print('Image is movie')

            self.pixmap_mv = QMovie(self.pattern_file)
            print('Check to see if gif valid: ', self.pixmap_mv.isValid())
            self.app_widget.setMovie(self.pixmap_mv)
            self.pixmap_mv.start()


if __name__ == "__main__":

    main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Zythyr
  • 1,142
  • 4
  • 20
  • 33
  • 1
    As you should know, you should not run the GUI in a secondary thread since nothing guarantees that it works, for example in my case none of the methods works, maybe you are using a version that does not indicate the errors and only hides the bugs but that will not happen in all cases. With the above implies that I will have to restructure your project so I need more information such as for example, why do you use time.sleep? Do you use it to generate a delay or to emulate tasks that consume time? Do you have any task that consumes a lot of time? – eyllanesc Dec 31 '19 at 06:45
  • @eyllanesc I used time.sleep to emulate another tasks that consumes time and or happens in parallel. Although I read in my places GUI should not be run in a secondary thread, this application is not a true GUI which has actual user input or interaction. It is solely used to display an image and update the image during various states of a physical process that is controlled by Python code. The context in which I would like to use this application can be understood from this question https://stackoverflow.com/questions/58927021/how-to-display-image-on-secondary-monitor-in-full-screen. – Zythyr Dec 31 '19 at 07:32
  • An application that displays an image is also a GUI. On the other hand, for not respecting the Qt rule you are having those problems that perhaps in the initial case it was only an image without change, it was not noticed that the window was frozen but when placing a dynamic element as a gif if it shows. – eyllanesc Dec 31 '19 at 07:36
  • @eyllanesc Why is it the first code I posted (two class approach) works as I expect it to work? If I can't run the GUI in a second thread, then what is the suggested solution? If I run the GUI in main thread, running app.exec_() blocks the rest of my logic/tasks from occurring... – Zythyr Dec 31 '19 at 07:43
  • 1) You have been lucky since Qt does not guarantee that it works, that is, it can work sometimes if and sometimes not, 2) I am working on publishing a correct solution to your previous question. – eyllanesc Dec 31 '19 at 07:45

1 Answers1

0

In a similar way to my previous answer you must execute the heavy tasks in another thread and use the signals to notify the changes:

import sys
import time


from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, Qt, QThread, QTimer
from PyQt5.QtGui import QColor, QPixmap, QMovie
from PyQt5.QtWidgets import QApplication, QLabel


class TaskManager(QObject):
    task3Finished = pyqtSignal()
    task4Finished = pyqtSignal()
    task5Finished = pyqtSignal()

    @pyqtSlot()
    def task(self):
        print("Step 3")
        time.sleep(2)
        self.task3Finished.emit()
        print("Step 4")
        time.sleep(1)
        self.task4Finished.emit()
        print("Step 5")
        time.sleep(1)
        self.task5Finished.emit()
        time.sleep(3)
        QApplication.quit()


class qtAppWidget(QLabel):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.app = QApplication.instance()
        self.screens_available = self.app.screens()
        self.screen = self.screens_available[0]
        self.screen_width = self.screen.size().width()
        self.screen_height = self.screen.size().height()

        self.setupGUI()

    def setupGUI(self):
        scale_window = 2

        self.setGeometry(
            0, 0, self.screen_width / scale_window, self.screen_height / scale_window
        )
        self.setWindowTitle("myImageDisplayApp")
        self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
        self.show()

    @pyqtSlot()
    def on_task3_finished(self):
        pixmap = QPixmap("qt_test_static.png")
        self.setPixmap(pixmap)

    @pyqtSlot()
    def on_task4_finished(self):
        movie = QMovie("qt_test_movie.gif", parent=self)
        self.setMovie(movie)
        movie.start()


def main(args):
    print("Step 1")
    print("     Some logic here without QT")

    print("Step 2")
    print("     Launch QT app to run")
    app = QApplication(args)
    myapp = qtAppWidget()

    thread = QThread()
    thread.start()

    manager = TaskManager()
    # move the QObject to the other thread
    manager.moveToThread(thread)

    manager.task3Finished.connect(myapp.on_task3_finished)
    manager.task4Finished.connect(myapp.on_task4_finished)

    # start task
    QTimer.singleShot(0, manager.task)

    ret = app.exec_()

    thread.quit()
    thread.wait()

    del thread, app

    return ret


if __name__ == "__main__":

    sys.exit(main(sys.argv))
eyllanesc
  • 235,170
  • 19
  • 170
  • 241