PyQt threading loop on start

Question:

I have been trying to get threading going with PyQt6 without much luck. I have explored multiple solutions, followed some tutorials and videos as well as looked at various questions previously asked by other users. Still, I do not seem to find a solution to make my code work.

I do understand that this could be a duplicate question as there are over 1000 questions about this topic. Therefore, I apologise if I have missed the right one.

All the solutions I have explored use __init__ or super.__init__, and what it might be done goes beyond my knowledge, I am new to OOP, and I would like to understand more about how to implement a loop inside the "GUI loop" that automatically starts when the app launches.

Could anyone kindly provide an example or guide me in the right direction on how to implement this function inside my code?

from PyQt6 import QtCore, QtGui, QtWidgets
from PyQt6.QtCore import Qt, QThread, pyqtSignal
import qdarktheme
import datetime
from threading import Thread

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName('MainWindow')
        MainWindow.resize(491, 590)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName('centralwidget')
        self.groupBox = QtWidgets.QGroupBox(self.centralwidget)
        self.groupBox.setGeometry(QtCore.QRect(30, 100, 201, 161))
        self.groupBox.setObjectName('groupBox')
        self.timeLineEdit = QtWidgets.QLineEdit(self.groupBox)
        self.timeLineEdit.setGeometry(QtCore.QRect(10, 50, 181, 20))
        self.timeLineEdit.setObjectName('timeLineEdit')
        self.timeLabel = QtWidgets.QLabel(self.groupBox)
        self.timeLabel.setGeometry(QtCore.QRect(10, 30, 181, 10))
        self.timeLabel.setObjectName('timeLabel')
        self.saveButton = QtWidgets.QPushButton(self.centralwidget)
        self.saveButton.setGeometry(QtCore.QRect(300, 530, 121, 21))
        self.saveButton.setObjectName('saveButton')
        self.saveButton.clicked.connect(self.displayTime)
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 491, 18))
        self.menubar.setObjectName('menubar')
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName('statusbar')
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate('MainWindow', 'TestApp'))
        self.groupBox.setTitle(_translate('MainWindow', 'Time'))
        self.timeLabel.setText(_translate('MainWindow', 'Time'))
        self.saveButton.setText(_translate('MainWindow', 'Save'))

    def displayTime(self):
        time_now = datetime.datetime.now()
        retrieved_time = time_now.strftime('%H:%M:%S %p')
        get_time = self.timeLineEdit.setText(retrieved_time)
        print(retrieved_time)
        
    MyThread = Thread(target=displayTime)
    
    
if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)
    
    def onStart():
        print('START THREAD')
        MyThread.start()
    
    QtCore.QTimer.singleShot(0, onStart)
    qdarktheme.setup_theme('light')
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec())

At the moment, the code does not provide a good solution (and is not working either) as it uses the threading library, and I would like to implement something using QThread; what I am trying to do now is to get onStart which is called as the app launches to start displayTime which is supposed to show the live time in timeLineEdit. I know that Qt has its own time, and that is a better approach, although showing the time is just an example of having an external value that is updated frequently.

UPDATE:

Following Ahmed AEK, I was able to set up a timer as follows:

if __name__ == '__main__':
    import sys

    def event_timer():
        time = datetime.datetime.now()
        global display_time
        display_time = time.strftime("%H:%M:%S %p")
        print(display_time)

    app = QtWidgets.QApplication(sys.argv)
    
    timer = QtCore.QTimer()
    timer.timeout.connect(event_timer)
    timer.start(1000)
                            
    #QtCore.QTimer.singleShot(0, onStart)
    qdarktheme.setup_theme('light')
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    
    MainWindow.show()
    sys.exit(app.exec())

Now everything works as expected, although the app crashes if I try to retrieve the time information inside MainWindow as follow:

def displayTime():
    print(display_time)
   

if __name__ == '__main__':
    import sys

    def event_timer():
        time = datetime.datetime.now()
        global display_time
        display_time = time.strftime("%H:%M:%S %p")
        ui.displayTime()
        #print(display_time)

What am I missing?

Ideally I would like displayTime to run self.timeLineEdit.setText(display_time)

Asked By: flxx

||

Answers:

GUI applications are made to work in only 1 thread, using threading on any GUI related function is an unidentified behavior and will result in a segmentation fault most of the time, or you could be more unlucky and the segmentation fault will happen some time later and you won’t find the caused.

the right direction is to not use threads here at all, and just run the function that updates the GUI in the main thread.

if __name__ == '__main__':
    import sys

    app = QtWidgets.QApplication(sys.argv)


    qdarktheme.setup_theme('light')
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)

    # the following code will run in he main thread and can update the GUI.
    def onStart():
        print('START THREAD')
        ui.displayTime()

    QtCore.QTimer.singleShot(0, onStart)

    MainWindow.show()
    sys.exit(app.exec())

if the logic is more complicated you can use signals to communicate back to the main thread to do the GUI updates Updating GUI elements in MultiThreaded PyQT, but you must not update the GUI from a child thread.

a looping function can re-emit itself to allow the GUI to update itself before calling this function again.

    def displayTime(self):
        time_now = datetime.datetime.now()
        retrieved_time = time_now.strftime('%H:%M:%S %p')
        get_time = self.timeLineEdit.setText(retrieved_time)
        print(retrieved_time)
        if check_the_condition_you_would_put_in_the_loop():
            self.some_signal_that_is_connected_to_this_function.emit()

a function that just updates time is better called using a periodic Qtimer, which is the main reason the timer exists.

Answered By: Ahmed AEK