简体   繁体   English

PyQt 在不阻塞主线程的情况下延迟函数的计算

[英]PyQt delaying function's computation without blocking main thread

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.我继承了一个 GUI 代码,其结构如下:任何按钮信号都会触发一个插槽,这些插槽然后调用外部进程来接收信息并等待该进程完成,然后插槽继续。 the issue is, this external process takes between 0.5 to 60 seconds, and in that time the GUI freezes.问题是,这个外部过程需要 0.5 到 60 秒,在这段时间内 GUI 冻结。 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.我正在努力寻找一种好方法来将此进程调用分离到不同的线程或 QProcess(这样我就不会阻塞主事件循环),然后从同一点返回并继续相关插槽(或函数)从该外部缓慢进程接收到的信息。 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?有没有一种 Qt 方法可以“产生”一个函数,直到该过程完成然后继续该函数?

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


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!")

在此处输入图像描述

You can use QThread.您可以使用 QThread。 With Qthread you can pass arguments to a function in mainWindow with signal mechanism.使用 Qthread,您可以通过信号机制将参数传递给 mainWindow 中的函数。

Here is a source that explains how to use Qthread:这是解释如何使用 Qthread 的来源:

https://realpython.com/python-pyqt-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):页面中有一个示例 gui,我把它写下来给你(你可以运行它):

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())

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.在我的示例中,我有 3 个按钮。 One that waits for one second, another that waits for 5, and another that waits for 10.一个等待一秒,另一个等待 5 秒,另一个等待 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).当按钮槽被单击时,我将它们连接到 threaded_wait() 并使用 lambda,因为我想向该方法传递一个关于等待多长时间的整数参数(在此示例中等待只是假处理时间)。

Then I have the method actual_wait() which is the code that is actually waiting, which is being executed by the thread.然后我有方法 actual_wait() ,它是实际等待的代码,正在由线程执行。 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由于有一个线程在运行该代码,因此主 GUI 事件循环在启动线程后立即退出 threaded_wait() 方法,并允许继续它的事件循环

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.这可以防止我的 GUI 冻结。

在此处输入图像描述

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.并在需要的地方检查 self.DONE 标志。 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.我认为如果你使用 QThread,你也可以在线程完成时发出一个信号,然后将该信号连接到任何插槽,但我没有使用 QThread。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM