1

I've inhereted a GUI code which is structured something like this: any button signal triggers a slot, those slots then call an external process to receive information and wait until that process finishes, then the slot proceeds. the issue is, this external process takes between 0.5 to 60 seconds, and in that time the GUI freezes. i'm struggling to find a good way to seperate this process call to a different thread or QProcess (that way i will not block the main event loop) and then return and continue the relevent slot (or function) from that same point with the information received from that external slow process. generators seem like something that should go here, but im struggling to figure how to restructure the code so this will work. any suggestions or ideas? is there a Qt way to "yield" a function until that process completes and then continue that function?

Psudo code of the current structure:

    button1.clicked.connect(slot1)
    button2.clicked.connect(slot2)
    
    def slot1():
        status = subprocess.run("external proc") # this is blocking
        ...
        ...
        return

    def slot2():
        status = subprocess.run("external proc") # this is blocking
        ...
        ...
        return


David DS
  • 41
  • 8

3 Answers3

0

Here is the code with the example I was mentioning in the comments:

class MainWindow(QMainWindow, ui_MainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        ui_MainWindow.__init__(self)
        self.setupUi(self)
    
        self.button_1.clicked.connect(lambda: self.threaded_wait(1))
        self.button_5.clicked.connect(lambda: self.threaded_wait(5))
        self.button_10.clicked.connect(lambda: self.threaded_wait(10))
    
        #Start a timer that executes every 0.5 seconds
        self.timer = QtCore.QBasicTimer()                                               
        self.timer.start(500, self)
    
        #INIT variables
        self.results = {}
        self.done = False
   
    def timerEvent(self, event):
        #Executes every 500msec.
        if self.done:
            print(self.results)
            self.done = False
    
   
    def threaded_wait(self, time_to_wait):
        self.done = False
        new_thread = threading.Thread(target=self.actual_wait, args=(time_to_wait,self.sender().objectName()))
        new_thread.start()
    
    def actual_wait(self, time_to_wait: int, button_name):
        print(f"Button {button_name} Pressed:\nSleeping for {int(time_to_wait)} seconds")

        time_passed = 0
    
        for i in range(0, time_to_wait):
            print(int( time_to_wait - time_passed))
            time.sleep(1)
            time_passed = time_passed + 1
    
        self.results[button_name] = [1,2,3,4,5]
        self.done = True
        print("Done!")

enter image description here

Andew
  • 321
  • 1
  • 9
0

You can use QThread. With Qthread you can pass arguments to a function in mainWindow with signal mechanism.

Here is a source that explains how to use Qthread:

https://realpython.com/python-pyqt-qthread/

if you read the soruce it will be helpfull to you, i think. And there is a sample gui in the page, i write it down to you(you can run it):

from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import QMainWindow
import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)
import sys
# Snip...

# Step 1: Create a worker class
#
class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def run(self):
        """Long-running task."""
        for i in range(5):
            time.sleep(1)
            self.progress.emit(i + 1)
        self.finished.emit()

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.clicksCount = 0
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Freezing GUI")
        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        # Create and connect widgets
        self.clicksLabel = QLabel("Counting: 0 clicks", self)
        self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.stepLabel = QLabel("Long-Running Step: 0")
        self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.longRunningBtn = QPushButton("Long-Running Task!", self)
        self.longRunningBtn.clicked.connect(self.runLongTask)
        # Set the layout
        layout = QVBoxLayout()
        layout.addWidget(self.clicksLabel)
        layout.addWidget(self.countBtn)
        layout.addStretch()
        layout.addWidget(self.stepLabel)
        layout.addWidget(self.longRunningBtn)
        self.centralWidget.setLayout(layout)

    def countClicks(self):
        self.clicksCount += 1
        self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")

    def reportProgress(self, n):
        self.stepLabel.setText(f"Long-Running Step: {n}")

    def runLongTask(self):
        # Step 2: Create a QThread object
        self.thread = QThread()
        # Step 3: Create a worker object
        self.worker = Worker()
        # Step 4: Move worker to the thread
        self.worker.moveToThread(self.thread)
        # Step 5: Connect signals and slots
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        # Step 6: Start the thread
        self.thread.start()

        # Final resets
        self.longRunningBtn.setEnabled(False)
        self.thread.finished.connect(
            lambda: self.longRunningBtn.setEnabled(True)
        )
        self.thread.finished.connect(
            lambda: self.stepLabel.setText("Long-Running Step: 0")
        )



app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
lufsasc
  • 71
  • 3
-1

Usually what I do is have the button press run a function that launches a thread to do the work for me.

In my example I have 3 buttons. One that waits for one second, another that waits for 5, and another that waits for 10.

I connect the button slots when they are clicked to threaded_wait() and I use lambda because I want to pass that method an integer argument on how long to wait for (Waiting in this example is just fake processing time).

Then I have the method actual_wait() which is the code that is actually waiting, which is being executed by the thread. Since there is a thread running that code, the main GUI event loop exits the threaded_wait() method right after starting the thread and it is allowed to continue it's event loop

class MainWindow(QMainWindow, ui_MainWindow):
    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        ui_MainWindow.__init__(self)
        self.setupUi(self)
    
        self.button_1.clicked.connect(lambda: self.threaded_wait(1))
        self.button_5.clicked.connect(lambda: self.threaded_wait(5))
        self.button_10.clicked.connect(lambda: self.threaded_wait(10))
    
    def threaded_wait(self, time_to_wait):
        new_thread = threading.Thread(target=self.actual_wait, args=(time_to_wait,))
        new_thread.start()
    
    def actual_wait(self, time_to_wait: int):
        print(f"Sleeping for {int(time_to_wait)} seconds")

        time_passed = 0
    
        for i in range(0, time_to_wait):
            print(int( time_to_wait - time_passed))
            time.sleep(1)
            time_passed = time_passed + 1
    
        print("Done!")

This prevents my GUI from freezing up.

enter image description here

EDIT:

Sorry as for the second part of your question, if you want to wait for the thread to finish before doing something else, you can use a flag like this:

def actual_wait(self, time_to_wait: int):
    print(f"Sleeping for {int(time_to_wait)} seconds")

    ....
    
    self.DONE = True

And check that self.DONE flag wherever you need it. It kind of depends what you mean by wait for it to complete. I think if you use QThread you can also emit a signal when the thread is done and connect that signal to whatever slot after that, but I haven't used QThread.

Andew
  • 321
  • 1
  • 9
  • Just a heads up I imported threading to use the threads, but I believe PyQt5 supports threading using QThread, which should work similarly and might play nicer with the GUIs as well. – Andew Jun 16 '22 at 15:20
  • So, i did do a similar solution to external processes that i do not need to recieve an answer from them before i proceed with more main thread code. but i cant understand how your solution solves the problem of receiving an answer from that waiting thread, then proceed to do more code. in your example threaded_wait has nothing else to do after the thread is started, the issue is, what if you do, and how do you make it continue later from that point, and do it only once. because i have like 20 buttons, so i need many to one to many ratio of signal to slots connections – David DS Jun 16 '22 at 15:33
  • so for example, i can solve my issue with your solution by creating a middle slot, which will generate a Qprocess and once that Qprocess finishes, trigger the intended button slot. but the issue is i will need a dedicated middle slot for each button and a dedicated Qprocess for each button, because i dont know how to make the Qprocess "know" which button called it and by that which corresponding slot to trigger once its finished – David DS Jun 16 '22 at 15:34
  • As sorry I didn't understand you wanted to solve the return problem not just the issue with the GUI freezing... Similar to how I set the flag self.DONE, you can just put data in any structure you'd like within the class. for example a dictionary like 'self.return_data[button_number] = data' and you can figure out which button was clicked causing the event in the first place with 'self.sender()' – Andew Jun 16 '22 at 15:39
  • I guess I just don't know how you use the returned data and how you'd like it to be returned. If you wait for the thread to finish before continuing code in the main event loop the GUI will always freeze. You need to create a signal and slot for the end of the thread and go to a class bound data structure when that happens, or use something like a queue which you check periodically on a timer or something along those lines. – Andew Jun 16 '22 at 15:42
  • or maybe something here will help: https://stackoverflow.com/questions/21936597/blocking-and-non-blocking-subprocess-calls – Andew Jun 16 '22 at 15:44
  • i guess what i want to do, or trying to do, is something of the style: button_signal ->QProc slot->finished_signal-> button_slot and the challange is , given that i have many buttons, many button_slots, but only one QProc slot that always does the same, how do i connect finished_signal to the correct button_slot? I guess i can go by the naive way, and just create a slot that has many many ifs, but this seems like a clumsy implementation, and i somewhat expect Qt or python will have a more elegent, generic and clean solution to this – David DS Jun 16 '22 at 16:01