Search code examples
pythonuser-interfacetimer

How to display a timer using PyQt5 for Python


I'm making a game where I would like a timer to be displayed on the screen after the user clicks on 'NEW GAME', to keep track of how long they've been playing. I have a class that runs the timer fine by itself, but when I incorporate it into the rest of my game and then on top of that, try to display the updated values of the timer, no values in the UI are updated and the printout of the timer doesn't even occur in the terminal. I've tried running the timer in the same thread as the game-setup process and I've also tried creating a new thread to run the timer but neither work. The game loads up and functions fine, with the exception of the timer not counting upwards and not displaying the updated timer values. Where am I going wrong here?

Here is my standalone Timer class, which again, works fine by itself.

from PyQt5 import QtCore
import sys

def startThread(functionName, *args, **kwargs):
    print(args)
    if len(args) == 0:
        t = threading.Thread(target=functionName)
    else:
        try:
            t = threading.Thread(target=functionName, args=args, kwargs=kwargs)
        except:
            try:
                if args is None:
                    t = threading.Thread(target=functionName, kwargs=kwargs)
            except:
                t = threading.Thread(target=functionName)
    t.daemon = True
    t.start()

class Timer(object):

    def __init__(self):
        super(Timer, self).__init__()

    def start_timer(self):
        print("Starting timer...")
        Timer.timer = QtCore.QTimer()
        Timer.time = QtCore.QTime(0, 0, 0)
        Timer.timer.timeout.connect(self.tick)
        Timer.timer.start(1000)

    def tick(self):
        Timer.time = Timer.time.addSecs(1)
        self.update_UI('%s' % Timer.time.toString("hh:mm:ss"))

    def update_UI(self, text_string):
        print(text_string)
        # This is where the text would be sent to try and update the UI

Timer().start_timer()

This is more or less how my game-setup class is structured - currently I'm showing the version that uses threading:

class BuildUI(PyQt5.QtWidgets.QMainWindow, Ui_game):

    def __init__(self):
        super(BuildUI, self).__init__()
        self.setupUi(self)
        self.easy_mode = 38
        self.user_available_cells = []
        self.start_easy_game.triggered.connect(lambda: self.setup_game(self.easy_mode))

    def setup_game(self, hidden_count):
        def create_game_board():
            startThread(Timer().start_timer)
            self.game = BuildGame()
                    #The BuildGame class is not deliberately not shown

        startThread(create_game_board)

class GAME(object):
    def __init__(self):
        GAME.app = PyQt5.QtWidgets.QApplication(sys.argv)
        GAME.UI = BuildUI()
        GAME.UI.show()
        GAME.app.exec_()

def main():
    GAME()

if __name__ == '__main__':
    main()

Solution

  • The key to getting this to work is by using signals. Leaving the Timer class exactly as it is, the only modifications to be done are in the initialization of the GAME class, a signal needs to be added at the beginning of the BuildUI class and then using emit() to trigger that signal just before the self.game = BuildGame() call.

    class BuildUI(PyQt5.QtWidgets.QMainWindow, sudoku_ui.Ui_sudoku_game):
        # This signal just triggers a msgbox to display, telling the user the game is loading
        process_start = PyQt5.QtCore.pyqtSignal()
        # this is called to automatically close the msgbox window       
        process_finished = PyQt5.QtCore.pyqtSignal()
        # This signal, when called will start the timer
        start_game_timer = PyQt5.QtCore.pyqtSignal()
    
        def __init__(self):
            super(BuildUI, self).__init__()
            self.setupUi(self)
            self.easy_mode = 38
            self.user_available_cells = []
            self.start_easy_game.triggered.connect(lambda: self.setup_game(self.easy_mode))
    
        def setup_game(self, hidden_count):
            def create_game_board():
                self.game = BuildGame()
                # Now that the game is built, the timer can start
                # This is the emit which will start the timer
                GAME.UI.start_game_timer.emit()
    
            startThread(create_game_board)
    
    class GAME(object):
        def __init__(self):
            GAME.app = PyQt5.QtWidgets.QApplication(sys.argv)
            GAME.UI = BuildUI()
            GAME.dialog_box = MsgPrompt()
    
            # This is the key right here - initializing the timer class
            GAME.timer = Timer()
            # This line below attaches a function to the emit() call - which will kick off the timer
            GAME.UI.start_game_timer.connect(GAME.timer.start_timer)
            # The below referenced class is deliberately omitted from this post
            GAME.UI.process_start.connect(GAME.dialog_box.show_dialog_box)
            GAME.UI.process_finished.connect(GAME.dialog_box.hide_dialog_box)
            GAME.UI.show()
            GAME.app.exec_()
    
    def main():
        GAME()
    
    if __name__ == '__main__':
        main()