简体   繁体   中英

PyQt5 signal communication between worker thread and main window is not working correctly

I'm creating an application with PyQt5, which has a very simple user interface. There is a dropdown list from which a value can be chosen, and then there is a button which can be clicked on and an action will be performed based on a current value in the dropdown list.

Currently if I run the code below, I can get a value from the dropdown list and it will be emitted to worker thread before myaction.myaction(customer_name) is run. The code also runs fine, but the GUI is not working the way I want it to work while the function in worker thread runs. When I click on the start button, it should emit a signal to the GUI to disable the button, change its label and color, but that never happens. When the function finishes, it should be changed back to its original form.

Is the problem how I handle signals, or am I having redundant functions in my classes? What is the correct way to send that dropdown list value to worker thread everytime the button is clicked, so I can simply use it as a variable there?

Every possible solution to this I find online gets me excited but none of them have been working for me yet, or some of them have been too confusing for me to understand.

Here's some answers I've already gone through


#!/usr/bin/env python3

import sys
#import myaction
import time
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton, QComboBox, QLabel
from PyQt5 import QtCore

class ConfWorker(QtCore.QThread):
    updated_button = QtCore.pyqtSignal(list)
    updated_label = QtCore.pyqtSignal(str)
    updated_error = QtCore.pyqtSignal(str)
    request_signal = QtCore.pyqtSignal()
    customer = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(ConfWorker, self).__init__(parent)
        self.customer.connect(self.getcustomer)

    def run(self):
        self.request_signal.emit()

    def getcustomer(self, text):
        self.configure(text)

    def configure(self, customer_name):
        self.updated_button.emit(["In progress...", False])
        self.updated_label.emit(customer_name)
        time.sleep(5) # During this time you should be able to see color change etc.
        #myaction.myaction(customer_name)# TAKES ~20 SECONDS TO FINISH
        self.updated_button.emit(["Start", True])

class ConfGUI(QWidget):
    def __init__(self, parent=None):
        super(ConfGUI, self).__init__()

        self.worker = ConfWorker(self)
        self.worker.updated_button.connect(self.updateButton)
        self.worker.updated_label.connect(self.updateLabel)
        self.worker.updated_error.connect(self.updateError)
        self.worker.request_signal.connect(self.sendCustomer)

        self.targetBtn = QPushButton('Start Configuration', self)
        self.targetBtn.setStyleSheet("QPushButton { background-color: green; color: white }"
                        "QPushButton:disabled { background-color: red; color: white }")
        self.targetBtn.clicked.connect(self.worker.start)
        self.targetBtn.setGeometry(100, 400, 200, 50)
        self.setGeometry(800, 300, 400, 550)
        self.setFixedSize(400, 550)

        self.customerlist = QComboBox(self)
        self.customerlist.setGeometry(100, 50, 200, 50)
        self.customerlist.setObjectName("customer")
        self.customerlist.addItem("testcustomer1")
        self.customerlist.addItem("testcustomer2")
        self.customerlist.addItem("testcustomer3")

        self.label = QLabel(self)
        self.label.setText("")
        self.label.setStyleSheet('font-size: 30pt; font-family: Courier; color: green;')
        self.label.setGeometry(70,250,400,50)

        self.error_label = QLabel(self)
        self.error_label.setText("")
        self.error_label.setStyleSheet('font-size: 30pt; font-family: Courier; color: red;')
        self.error_label.setGeometry(70,350,400,50)

        self.show()

    def sendCustomer(self):
        self.worker.customer.emit(self.customerlist.currentText())

    def updateButton(self, button_list):
        self.targetBtn.setText(button_list[0])
        self.targetBtn.setEnabled(button_list[1])

    def updateLabel(self, label_text):
        self.label.setText(label_text)

    def updateError(self, error_text):
        self.error_label.setText(error_text)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = ConfGUI()
    sys.exit(app.exec_())

The problem is caused by a very common and wrong conception, they think that QThread is a Qt Thread , that is, a new thread made by Qt, but no, QThread is a thread handler as indicated in the docs :

The QThread class provides a platform-independent way to manage threads.

A QThread object manages one thread of control within the program. QThreads begin executing in run(). By default, run() starts the event loop by calling exec() and runs a Qt event loop inside the thread.

The only part that runs on another thread is the run method, and in your case you are not calling because your logic is different, you do not want to continuously execute a heavy task but at the user's request, so design must be that of a worker but you are using a QThread as the base class, and that is incorrect, you must use a QObject as a base class and move it to a new thread so that the QObject does its tasks in that thread, preventing the GUI from blocking.

A QObject is not thread-safe, it lives in a thread, and the thread where it is determined by the following:

  • A QObject lives in the parent's thread

  • If you do not have a parent, live in the thread where it was created unless you have moved to another thread.

  • And you can move to another thread with the moveToThread() function, but moveToThread() will fail if the QObject has a parent since the first criterion is privileged.

On the other hand if you want to call a method from another thread it is necessary that you use the decorator @QtCore.pyqtSlot()


Considering the above, we obtain the following solution:

#!/usr/bin/env python3
import sys
import time
from PyQt5 import QtCore, QtWidgets


class ConfWorker(QtCore.QObject):
    updated_button = QtCore.pyqtSignal(list)
    updated_label = QtCore.pyqtSignal(str)
    updated_error = QtCore.pyqtSignal(str)
    request_signal = QtCore.pyqtSignal()
    customer = QtCore.pyqtSignal(str)

    def __init__(self, parent=None):
        super(ConfWorker, self).__init__(parent)
        self.customer.connect(self.getcustomer)

    @QtCore.pyqtSlot()
    def doWork(self):
        self.request_signal.emit()

    @QtCore.pyqtSlot(str)
    def getcustomer(self, text):
        self.configure(text)

    def configure(self, customer_name):
        self.updated_button.emit(["In progress...", False])
        self.updated_label.emit(customer_name)
        time.sleep(5) # During this time you should be able to see color change etc.
        #myaction.myaction(customer_name)# TAKES ~20 SECONDS TO FINISH
        self.updated_button.emit(["Start", True])

class ConfGUI(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(ConfGUI, self).__init__()

        # create a QThread and start the thread that handles
        thread = QtCore.QThread(self)
        thread.start()

        # create the worker without a parent so you can move
        self.worker = ConfWorker()
        # the worker moves to another thread
        self.worker.moveToThread(thread)

        self.worker.updated_button.connect(self.updateButton)
        self.worker.updated_label.connect(self.updateLabel)
        self.worker.updated_error.connect(self.updateError)
        self.worker.request_signal.connect(self.sendCustomer)

        self.targetBtn = QtWidgets.QPushButton('Start Configuration', self)
        self.targetBtn.setStyleSheet("QPushButton { background-color: green; color: white }"
                        "QPushButton:disabled { background-color: red; color: white }")
        self.targetBtn.clicked.connect(self.worker.doWork)
        self.targetBtn.setFixedSize(200, 50)

        self.customerlist = QtWidgets.QComboBox(self)
        self.customerlist.addItems(["testcustomer1", "testcustomer2", "testcustomer3"])
        self.customerlist.setFixedSize(200, 50)

        self.label = QtWidgets.QLabel(self, alignment=QtCore.Qt.AlignCenter)
        self.label.setStyleSheet('font-size: 30pt; font-family: Courier; color: green;')
        self.label.setFixedSize(400,50)

        self.error_label = QtWidgets.QLabel(self, alignment=QtCore.Qt.AlignCenter)
        self.error_label.setStyleSheet('font-size: 30pt; font-family: Courier; color: red;')
        self.error_label.setFixedSize(400,50)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(self.customerlist, alignment=QtCore.Qt.AlignCenter)
        lay.addWidget(self.label, alignment=QtCore.Qt.AlignCenter)
        lay.addWidget(self.error_label, alignment=QtCore.Qt.AlignCenter)
        lay.addWidget(self.targetBtn, alignment=QtCore.Qt.AlignCenter)
        self.setFixedSize(400, 550)

    @QtCore.pyqtSlot()
    def sendCustomer(self):
        self.worker.customer.emit(self.customerlist.currentText())

    @QtCore.pyqtSlot(list)
    def updateButton(self, button_list):
        self.targetBtn.setText(button_list[0])
        self.targetBtn.setEnabled(button_list[1])

    @QtCore.pyqtSlot(str)
    def updateLabel(self, label_text):
        self.label.setText(label_text)

    @QtCore.pyqtSlot(str)
    def updateError(self, error_text):
        self.error_label.setText(error_text)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ex = ConfGUI()
    ex.show()
    sys.exit(app.exec_())

Observation: Also, as you can see in my solution, QThread will be a child of the window, because QThread is a QObject that handles another thread.

You really write very difficult simple things. Try it:

import sys
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton, QComboBox, QLabel
from PyQt5.QtCore    import QThread, pyqtSignal

class ConfWorker(QThread):
    threadSignal = pyqtSignal(str)
    finishSignal = pyqtSignal(str)

    def __init__(self, startParm):
        super().__init__()
        self.startParm = startParm   # Initialize the parameters passed to the task 

    def run(self):
        # Do something...
        for i in range(20):
            text = "In progress ................." \
                    if i%2==0 else "In progress {}".format(self.startParm)
            self.threadSignal.emit(text)
            QThread.msleep(500)
        self.finishSignal.emit(self.startParm)


class ConfGUI(QWidget):
    def __init__(self):
        super().__init__()
        self.setGeometry(800, 100, 400, 550)
        self.setFixedSize(400, 550)        

        self.targetBtn = QPushButton('Start Configuration', self)
        self.targetBtn.setStyleSheet(
                        "QPushButton { background-color: green; color: white;}"
                        "QPushButton:disabled { background-color: red; color: white;}"
                        )
        self.targetBtn.setGeometry(100, 400, 200, 50)
        self.targetBtn.clicked.connect(self.workerStart)           

        self.customerlist = QComboBox(self)
        self.customerlist.setGeometry(100, 50, 200, 50)
        self.customerlist.setObjectName("customer")
        self.customerlist.addItem("testcustomer1")
        self.customerlist.addItem("testcustomer2")
        self.customerlist.addItem("testcustomer3")
        self.customerlist.activated[str].connect(self.comboActivated)
        self.startParm = "testcustomer1"

        self.label = QLabel(self)
        self.label.setStyleSheet('font-size: 30pt; font-family: Courier; color: green;')
        self.label.setGeometry(70,250,400,50)

    def workerStart(self):
        self.targetBtn.setText("In progress...")
        self.targetBtn.setEnabled(False)
        self.label.setText("")
        self.worker = ConfWorker(self.startParm)  
        self.worker.threadSignal.connect(self.on_threadSignal)
        self.worker.finishSignal.connect(self.on_finishSignal)        
        self.worker.start()                     

    def on_threadSignal(self, text):
        self.targetBtn.setText(text)

    def on_finishSignal(self, text):
        self.targetBtn.setEnabled(True)
        self.targetBtn.setText("Start Configuration'")
        self.label.setText(text)

    def comboActivated(self, text):
        self.startParm = text


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex  = ConfGUI()
    ex.show()
    sys.exit(app.exec_())

在此输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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