Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Run Function in the Background and Update UI

Tags:

python

pyqt

pyqt5

I am using PyQt to make a GUI for a project.

Screenshot of GUI

After inputting a number and submitting it, I need to execute the function that would run in a background, otherwise the app freezes until the process is finished.

I also need to output logs in the dark box that are produced by the function.

This is the GUI code:

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

And the run function:

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

In order to run the function in the background, I have tried to use Process, but when I execute the append function, the GUI doesn't update.

like image 805
dan070 Avatar asked Dec 19 '22 02:12

dan070


1 Answers

The GUI should not be updated from another thread since Qt creates a loop where the application lives, although python provides many alternatives for works with threads, often these tools do not handle the logic of Qt so they can generate problems. Qt provides classes that perform this type of tasks with QThread (low-level), but this time I will use QRunnable and QThreadPool, I have created a class that behaves the same as 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)

Use:

self.p = ProcessRunnable(target=run, args=(user_input.displayText(), box))
self.p.start()

Also as I said before, you should not update the GUI directly from another thread, a solution is to use signals, or in this case, for simplicity, use 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))

To be invoked correctly this must be a slot, for this we use a decorator:

class LogginOutput(QTextEdit):
    # ...
    @pyqtSlot(str)
    def append(self, text):
        self.moveCursor(QTextCursor.End)
        # ...

The complete and workable example is in the following code

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_())
like image 131
eyllanesc Avatar answered Dec 27 '22 01:12

eyllanesc