Search code examples
pythonpyqt5pyside2qdialog

How to display a QDialog box with instructional text, while a process is running


Overview: I have a GUI-based program that has a multitude of functionality built into buttons and menu selection items. Many of the processes are instantaneous, but several of them can easily take more than 5, 10 or even 20 seconds.

Current Problem: While I have a dialog box displaying, it will not display the text inside of the box, such as 'Completing Process, please wait...'

Goal: While a long processes run, I would like to display a 'please wait' dialog box, so that the user knows the program is not stalled and is working on the function the user selected.

Background info: The GUI is built in PySide2 (aka PyQt5) and Python 3.6.5

Current Attempts:


Method 1 - Initially, I used the threading module to initiate the primary function to be run. At the start of the function, I would have a call to show the dialog box, which would display as intended and the rest of the function would process/complete in the background.

Method 1 Problem - This worked great until the program became more complex and I needed to use ThreadPoolExecutor to speed up things. ThreadPoolExecutor and the threading module do not work well, if at all together and will result in a crash without an error message, so I had to abandon that method.


Method 2 - I tried using the setModal function of the QDialog box and called the exec_ function

Method 2 Problem - The dialog box would show and display the text as desired, but setModal(False) had no effect and the processes would be halted until the window was closed.


Method 3 (current method used) - In the initialization of my ProcessRunning dialog window class, I have created a PySide2 signal, which takes in a string (the string being the message that I want to display). The preceeding line before a call to a long process, I called the emit() function of my signal, connected to which is the function to display the dialog box.

Method 3 Problem - The window displays and the background process runs but the window doesn't display the text, almost like that part of the process is held up by the background process.


Summary: While the question title suggests I don't know how to display a QDialog box, the real problem is displaying the text inside the window which instructs the user to "wait." I feel that since this isn't happening with my current method, I am not displaying the box "correctly". So, the "correct" method to achieve this is what I am in search of.

Here's a "short" example using concepts I have in my full program. :

import sys
import math
import PySide2
from PySide2 import QtCore, QtGui, QtWidgets
import pandas

class Ui_functionRunning(object):
    def setupUi(self, functionRunning):
        functionRunning.setObjectName("functionRunning")
        functionRunning.resize(234, 89)
        self.labelProcessStatus = QtWidgets.QLabel(functionRunning)
        self.labelProcessStatus.setGeometry(QtCore.QRect(10, 0, 221, 51))
        self.labelProcessStatus.setAlignment(QtCore.Qt.AlignCenter)
        self.labelProcessStatus.setWordWrap(True)
        self.labelProcessStatus.setObjectName("labelProcessStatus")
        self.buttonProcessCompleted = QtWidgets.QPushButton(functionRunning)
        self.buttonProcessCompleted.setEnabled(False)
        self.buttonProcessCompleted.setGeometry(QtCore.QRect(60, 60, 111, 23))
        self.buttonProcessCompleted.setObjectName("buttonProcessCompleted")

class FunctionRunning(PySide2.QtWidgets.QDialog, Ui_functionRunning):
    display_box = PySide2.QtCore.Signal(str)

    def __init__(self):
        super(FunctionRunning, self).__init__()
        self.setupUi(self)

    def showDialog(self, displayText):
        MasterClass.process_running.labelProcessStatus.setText(displayText)
        MasterClass.process_running.show()

class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(335, 255, 126, 23))
        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 21))
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        MainWindow.setStatusBar(self.statusbar)
        self.pushButton.setText("PUSH TO TEST ")

class MainWindowUI(PySide2.QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super(MainWindowUI, self).__init__()
        self.setupUi(self)
        self.pushButton.clicked.connect(self.test_function_running)

    def test_function_running(self):
        MasterClass.process_running.display_box.emit('testing running and displaying a message')        
        df = pandas.DataFrame({'id': [0,1,2,3,4,5,6,7,8,9],'lon':[-121.28473, -121.29511, -121.32834, -121.29569, -121.29251, -121.25374, -121.28417, -121.29854, -121.21188, -121.25812], 'lat': 
            [37.986450, 37.911396, 37.969345, 37.923443, 37.990696, 37.975395, 37.942062, 37.993350, 37.979430, 37.975790]})

        `If your system processes all this too quickly to notice the problem, increase the value in 
         `this first loop, from 10 to something like 20 or 30    
        for x in range(10):
            for i in df.index:
                for r in df.index:
                    if df.loc[i, 'lat'] != '':
                        df.loc[i, 'DIST_to_%s' % df.loc[i, 'id']] = self.dist(float(df.loc[i, 'lat']), float(df.loc[i, 'lon']), float(df.loc[r, 'lat']), float(df.loc[r, 'lon']))
                print('%s pass completed - %s times through' % (i, x))
        
        print('finished calculating distances')
        MasterClass.process_running.labelProcessStatus.setText('All Done!')        

    def dist(self, lat_1, lon_1, lat_2, lon_2):
        if lat_1 != lat_2 and lon_1 != lon_2:
            val_1 = math.radians(90 - float(lat_1))
            val_2 = math.cos(val_1)
            val_3 = math.radians(90 - float(lat_2))
            val_4 = math.cos(val_3)
            val_5 = math.radians(90 - float(lat_1))
            val_6 = math.sin(val_5)
            val_7 = math.radians(90 - float(lat_2))
            val_8 = math.sin(val_7)
            val_9 = math.radians(float(lon_1) - float(lon_2))
            val_10 = math.cos(val_9)
            distance = round(math.acos(val_2 * val_4 + val_6 * val_8 * val_10) * 3958.756, 1)
            return distance
        else:
            return 0

class MasterClass:

    def __init__(self):
        super(MasterClass, self).__init__()
        MasterClass.app = PySide2.QtWidgets.QApplication(sys.argv)
        MasterClass.process_running = FunctionRunning()
        MasterClass.process_running.display_box.connect(MasterClass.process_running.showDialog)
        MasterClass.main_ui = MainWindowUI()
        MasterClass.main_ui.show()
        MasterClass.app.exec_()

if __name__ == '__main__':
    MasterClass()

Solution

  • The Method 1 you reference may work but with a few considerations; any call to update your UI, should be done using signals, via PySide2.QtCore.Signal()

    Change your MainWindowUI class to look something like this (keep your dist function, no changes needed there):

    class MainWindowUI(PySide2.QtWidgets.QMainWindow, Ui_MainWindow):
        # Create your signal here
        initiate_function = PySide2.QtCore.Signal()
    
        def __init__(self):
            super(MainWindowUI, self).__init__()
            self.setupUi(self)
            self.pushButton.clicked.connect(self.test_function_running)
    
        def test_function_running(self):
            MasterClass.process_running.display_box.emit('testing running and displaying a message')
    
            def test():  
                df = pandas.DataFrame({'id': [0,1,2,3,4,5,6,7,8,9],'lon':[-121.28473, -121.29511, -121.32834, -121.29569, -121.29251, -121.25374, -121.28417, -121.29854, -121.21188, -121.25812], 'lat': 
                    [37.986450, 37.911396, 37.969345, 37.923443, 37.990696, 37.975395, 37.942062, 37.993350, 37.979430, 37.975790]})
    
                for x in range(10):
                    for i in df.index:
                        for r in df.index:
                            if df.loc[i, 'lat'] != '':
                                df.loc[i, 'DIST_to_%s' % df.loc[i, 'id']] = self.dist(float(df.loc[i, 'lat']), float(df.loc[i, 'lon']), float(df.loc[r, 'lat']), float(df.loc[r, 'lon']))
                        print('%s pass completed - %s times through' % (i, x))
                        # You could even update you dialog box mid-process
                        MasterClass.process_running.labelProcessStatus.setText('%s pass completed - %s times through' % (i, x))        
                
                print('finished calculating distances')
                # Below, the signal is emitted and simultaneously runs the function assigned below, in the MasterClass __init__
                MasterClass.main_ui.initiate_function.emit()
                MasterClass.process_running.labelProcessStatus.setText('All Done!')        
            
            t = threading.Thread(target=test)
            t.daemon = True
            t.start()
            
        def update_ui(self):
            print('Updating UI here')
            self.pushButton.setText('New Button Text')
    

    Now update your MasterClass to look like this:

    class MasterClass:
    
        def __init__(self):
            super(MasterClass, self).__init__()
            MasterClass.app = PySide2.QtWidgets.QApplication(sys.argv)
            MasterClass.process_running = FunctionRunning()
            MasterClass.process_running.display_box.connect(MasterClass.process_running.showDialog)
            MasterClass.main_ui = MainWindowUI()
            # After the MasterClass object is initialized, you can connect a function to the signal that was added to the MainWindowUI class
            MasterClass.main_ui.initiate_function.connect(MasterClass.main_ui.update_ui)
            # update_ui is the new function added above, all it does is change the text on the button, but it could be more complex if needed
            MasterClass.main_ui.show()
            MasterClass.app.exec_()
    

    This should help with using threads with PySide2 and also with problems of threading and ThreadPoolExecutor running into issues. So the key takeaway here, is use a signal when you need a process to be run in the main thread, such as an update to the UI. It can be used for other things, but is only absolutely necessary when the UI needs to be refreshed.