在后台运行函数并更新 UI

dan*_*070 3 python pyqt pyqt5

我正在使用 PyQt 为项目制作 GUI。

图形用户界面截图

输入数字并提交后,我需要执行将在后台运行的函数,否则应用程序会冻结,直到进程完成。

我还需要在函数产生的暗箱中输出日志。

这是图形用户界面代码:

import sys
from PyQt5.QtWidgets import (
    QWidget, 
    QDesktopWidget, 
    QLineEdit, 
    QGridLayout, 
    QLabel,
    QFrame,
    QPushButton,
    QApplication,
    QTextEdit
)
from PyQt5.QtGui import (QTextCursor)
from bot.bot import (run, slack_notification)
from multiprocessing import Process, Pipe

class LogginOutput(QTextEdit):
    def __init__(self, parent=None):
        super(LogginOutput, self).__init__(parent)

        self.setReadOnly(True)
        self.setLineWrapMode(self.NoWrap)

        self.insertPlainText("")

    def append(self, text):
        self.moveCursor(QTextCursor.End)
        current = self.toPlainText()

        if current == "":
            self.insertPlainText(text)
        else:
            self.insertPlainText("\n" + text)

        sb = self.verticalScrollBar()
        sb.setValue(sb.maximum())

class App(QWidget):
    def __init__(self):
        super().__init__()

        self.init_ui()

    def init_ui(self):
        label = QLabel('Amount')
        amount_input = QLineEdit()
        submit = QPushButton('Submit', self)
        box = LogginOutput(self)

        submit.clicked.connect(lambda: self.changeLabel(box, amount_input))

        grid = QGridLayout()
        grid.addWidget(label, 0, 0)
        grid.addWidget(amount_input, 1, 0)
        grid.addWidget(submit, 1, 1)
        grid.addWidget(box, 2, 0, 5, 2)

        self.setLayout(grid)
        self.resize(350, 250)
        self.setWindowTitle('GetMeStuff Bot v0.1')
        self.show()

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def changeLabel(self, box, user_input):
        p = Process(target=run, args=(user_input.displayText(), box))
        p.start()
        user_input.clear()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = App()
    sys.exit(app.exec_())
Run Code Online (Sandbox Code Playgroud)

run功能:

def run(user_input, log):
    if user_input == "":
        log.append("Please enter a value\n")
    else:
        log.append("Test")
Run Code Online (Sandbox Code Playgroud)

为了在后台运行该函数,我尝试使用Process,但是当我执行append函数时,GUI 不会更新。

eyl*_*esc 9

GUI 不应该从另一个线程更新,因为 Qt 在应用程序所在的位置创建了一个循环,尽管 python 为线程工作提供了许多替代方案,但这些工具通常不处理 Qt 的逻辑,因此它们可能会产生问题。Qt 提供了使用 QThread(低级)执行此类任务的类,但这次我将使用 QRunnable 和 QThreadPool,我创建了一个行为与 Process 相同的类:

class ProcessRunnable(QRunnable):
    def __init__(self, target, args):
        QRunnable.__init__(self)
        self.t = target
        self.args = args

    def run(self):
        self.t(*self.args)

    def start(self):
        QThreadPool.globalInstance().start(self)
Run Code Online (Sandbox Code Playgroud)

用:

self.p = ProcessRunnable(target=run, args=(user_input.displayText(), box))
self.p.start()
Run Code Online (Sandbox Code Playgroud)

同样正如我之前所说,你不应该直接从另一个线程更新 GUI,一个解决方案是使用信号,或者在这种情况下,为简单起见,使用QMetaObject.invokeMethod

def run(user_input, log):
    text = ""
    if user_input == "":
        text = "Please enter a value\n"
    else:
        text = "Test"

    QMetaObject.invokeMethod(log,
                "append", Qt.QueuedConnection, 
                Q_ARG(str, text))
Run Code Online (Sandbox Code Playgroud)

要正确调用,这必须是一个插槽,为此我们使用一个装饰器:

class LogginOutput(QTextEdit):
    # ...
    @pyqtSlot(str)
    def append(self, text):
        self.moveCursor(QTextCursor.End)
        # ...
Run Code Online (Sandbox Code Playgroud)

完整且可行的示例在以下代码中

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *

class ProcessRunnable(QRunnable):
    def __init__(self, target, args):
        QRunnable.__init__(self)
        self.t = target
        self.args = args

    def run(self):
        self.t(*self.args)

    def start(self):
        QThreadPool.globalInstance().start(self)

def run(user_input, log):
    text = ""
    if user_input == "":
        text = "Please enter a value\n"
    else:
        text = "Test"

    QMetaObject.invokeMethod(log,
                "append", Qt.QueuedConnection, 
                Q_ARG(str, text))

class LogginOutput(QTextEdit):
    def __init__(self, parent=None):
        super(LogginOutput, self).__init__(parent)

        self.setReadOnly(True)
        self.setLineWrapMode(self.NoWrap)
        self.insertPlainText("")

    @pyqtSlot(str)
    def append(self, text):
        self.moveCursor(QTextCursor.End)
        current = self.toPlainText()

        if current == "":
            self.insertPlainText(text)
        else:
            self.insertPlainText("\n" + text)

        sb = self.verticalScrollBar()
        sb.setValue(sb.maximum())

class App(QWidget):
    def __init__(self):
        super().__init__()

        self.init_ui()

    def init_ui(self):
        label = QLabel('Amount')
        amount_input = QLineEdit()
        submit = QPushButton('Submit', self)
        box = LogginOutput(self)

        submit.clicked.connect(lambda: self.changeLabel(box, amount_input))

        grid = QGridLayout()
        grid.addWidget(label, 0, 0)
        grid.addWidget(amount_input, 1, 0)
        grid.addWidget(submit, 1, 1)
        grid.addWidget(box, 2, 0, 5, 2)

        self.setLayout(grid)
        self.resize(350, 250)
        self.setWindowTitle('GetMeStuff Bot v0.1')
        self.show()

    def center(self):
        qr = self.frameGeometry()
        cp = QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def changeLabel(self, box, user_input):
        self.p = ProcessRunnable(target=run, args=(user_input.displayText(), box))
        self.p.start()
        user_input.clear()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    widget = App()
    sys.exit(app.exec_())
Run Code Online (Sandbox Code Playgroud)