PyQt5 threading GUI does not work

2019-02-20 17:55发布

问题:

I am trying to load some data which takes 30+ seconds. During this time I wish the user to see a small GUI which says "Loading .", then "Loading ..", then "Loading ...", then "Loading ." etc. I have done some reading and I think I have to put this in a separate thread. I found someone who had a similar problem suggesting the solution was this in the right spot:

t = threading.Thread(target=self.test)
t.daemon = True
t.start()

In a lower part of the file I have the test function

def test(self):
    tmp = InfoMessage()

    while True:
        print(1)

and the InfoMessage function

from PyQt5 import uic, QtCore, QtGui, QtWidgets
import sys

class InfoMessage(QtWidgets.QDialog):
    def __init__(self, msg='Loading ', parent=None):
        try:
            super(InfoMessage, self).__init__(parent)
            uic.loadUi('ui files/InfoMessage.ui',self)

            self.setWindowTitle(' ')

            self.o_msg = msg
            self.msg = msg
            self.info_label.setText(msg)
            self.val = 0

            self.timer = QtCore.QTimer()
            self.timer.setInterval(500)
            self.timer.timeout.connect(self.update_message)
            self.timer.start()

            self.show()
        except BaseException as e:
            print(str(e))

    def update_message(self):
        self.val += 1
        self.msg += '.'

        if self.val < 20:
            self.info_label.setText(self.msg)
        else:
            self.val = 0
            self.msg = self.o_msg

        QtWidgets.QApplication.processEvents()

def main():
    app = QtWidgets.QApplication(sys.argv)      # A new instance of QApplication
    form = InfoMessage('Loading ')                  # We set the form to be our MainWindow (design)
    app.exec_()                                 # and execute the app

if __name__ == '__main__':                      # if we're running file directly and not importing it
    main()                                      # run the main function

When I run the InfoMessage function alone it works fine and it updates every 0.5 seconds etc. However, when I fun this as part of the loading file the GUI is blank and incorrectly displayed. I know it is staying in the test function because of the print statement in there.

Can someone point me in the right direction? I think I am missing a couple of steps.

回答1:

First, there are two ways of doing this. One way is to use the Python builtin threading module. The other way is to use the QThread library which is much more integrated with PyQT. Normally, I would recommend using QThread to do threading in PyQt. But QThread is only needed when there is any interaction with PyQt.

Second, I've removed processEvents() from InfoMessage because it does not serve any purpose in your particular case.

Finally, setting your thread as daemon implies your thread will never stop. This is not the case for most functions.

import sys
import threading
import time

from PyQt5 import uic, QtCore, QtWidgets
from PyQt5.QtCore import QThread


def long_task(limit=None, callback=None):
    """
    Any long running task that does not interact with the GUI.
    For instance, external libraries, opening files etc..
    """
    for i in range(limit):
        time.sleep(1)
        print(i)
    if callback is not None:
        callback.loading_stop()


class LongRunning(QThread):
    """
    This class is not required if you're using the builtin
    version of threading.
    """
    def __init__(self, limit):
        super().__init__()
        self.limit = limit

    def run(self):
        """This overrides a default run function."""
        long_task(self.limit)


class InfoMessage(QtWidgets.QDialog):
    def __init__(self, msg='Loading ', parent=None):
        super(InfoMessage, self).__init__(parent)
        uic.loadUi('loading.ui', self)

        # Initialize Values
        self.o_msg = msg
        self.msg = msg
        self.val = 0

        self.info_label.setText(msg)
        self.show()

        self.timer = QtCore.QTimer()
        self.timer.setInterval(500)
        self.timer.timeout.connect(self.update_message)
        self.timer.start()

    def update_message(self):
        self.val += 1
        self.msg += '.'

        if self.val < 20:
            self.info_label.setText(self.msg)
        else:
            self.val = 0
            self.msg = self.o_msg

    def loading_stop(self):
        self.timer.stop()
        self.info_label.setText("Done")


class MainDialog(QtWidgets.QDialog):
    def __init__(self, parent=None):
        super(MainDialog, self).__init__(parent)

        # QThread Version - Safe to use
        self.my_thread = LongRunning(limit=10)
        self.my_thread.start()
        self.my_loader = InfoMessage('Loading ')
        self.my_thread.finished.connect(self.my_loader.loading_stop)

        # Builtin Threading - Blocking - Do not use
        # self.my_thread = threading.Thread(
        #     target=long_task,
        #     kwargs={'limit': 10}
        # )
        # self.my_thread.start()
        # self.my_loader = InfoMessage('Loading ')
        # self.my_thread.join()  # Code blocks here
        # self.my_loader.loading_stop()

        # Builtin Threading - Callback - Use with caution
        # self.my_loader = InfoMessage('Loading ')
        # self.my_thread = threading.Thread(
        #     target=long_task,
        #     kwargs={'limit': 10,
        #             'callback': self.my_loader}
        # )
        # self.my_thread.start()


def main():
    app = QtWidgets.QApplication(sys.argv)
    dialog = MainDialog()
    app.exec_()

if __name__ == '__main__':
    main()

Feel free to ask any follow up questions regarding this code.

Good Luck.

Edit: Updated to show how to run code on thread completion. Notice the new parameter added to long_task function.