Python Forum

Full Version: PyQt Threading & Class handling
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello there,

I am working on the small project in which I need to process data based on input numbers. Here is the code and questions below:

import sys
import convert
import db_analysis
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtCore import QThread
from PyQt5 import uic

Ui_MainWindow, QtBaseClass = uic.loadUiType('interface.ui')


class Window(QtBaseClass, Ui_MainWindow):

    def __init__(self, parent = None):

        super(QtBaseClass, self).__init__(parent)

        self.setupUi(self)
        self.threading = ThreadClass()

        self.push_import.clicked.connect(self.import_data)
        self.push_cancel.clicked.connect(self.close_app)
        self.push_ok.clicked.connect(self.process_input)


    def update_progress_bar(self, progress_val):
        self.input_progress_bar.setValue(progress_val)

    def import_data(self):
        db_analysis.insert_data()

    def process_input(self):
        self.threading.start()

    def closeEvent(self, event):
        odp = QMessageBox.question(
            self, 'Exit',
            "Are you sure you want exit?",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

        if odp == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()

    def close_app(self):
        odp = QMessageBox.question(
            self, 'Exit',
            "Are you sure you want exit?",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

        if odp == QMessageBox.Yes:
            sys.exit()


class ThreadClass(QThread):

    def __init__(self, parent=None):
        super(ThreadClass, self).__init__(parent)

    def run(self):
        convert.convert_main()


def main():
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()
As you see I am using threading in PyQt to be able to update progress bar widget value. Few things I am struggling with:

1. How can I improve my existing code so I can have quick access to the list of widgets from interface.ui (auto-complete after self in __init__). Atm I need to write it down or copy from interface.py

2. After input value in input_line (QLineEdit) I would like to pass self.input_line.text() into convert.convert_main() inside run() function of ThreadClass.

3. After push_import click Widow Explorer dialog window pop up, when I choose correct file code is finish properly, but when I want to close the dialog window before choosing file whole program close instead to going back to main GUI. How I should handle with this event?
(Mar-02-2019, 06:02 PM)mrdominikku Wrote: [ -> ]1. How can I improve my existing code so I can have quick access to the list of widgets from interface.ui (auto-complete after self in __init__). Atm I need to write it down or copy from interface.py


You can import the ui file with uic, and place it inside an object (self.ui) to access it later:

#!/usr/bin/python3
import os
import sys
from PyQt5 import QtCore, QtGui, QtWidgets , uic

LOCAL_DIR = os.path.dirname(os.path.realpath(__file__)) + "/"


class Main(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui = uic.loadUi(LOCAL_DIR + "gui_main.ui", self)
        # print(self.ui.objectName.text()) ...
        self.show()


if __name__== '__main__':
    app = QtWidgets.QApplication([])
    gui = Main()
    sys.exit(app.exec_())
(Mar-02-2019, 06:02 PM)mrdominikku Wrote: [ -> ]2. After input value in input_line (QLineEdit) I would like to pass self.input_line.text() into convert.convert_main() inside run() function of ThreadClass.


See this example on how to do threading in pyqt5. The correct way is to use the moveToThread method and to communicate with the thread by using the signals and slots mechanism.

#!/usr/bin/python3
# Threading example with QThread and moveToThread (PyQt5)
import sys
import time
from PyQt5 import QtWidgets, QtCore

class WorkerThread(QtCore.QObject):
    signalExample = QtCore.pyqtSignal(str, int)

    def __init__(self):
        super().__init__()

    @QtCore.pyqtSlot()
    def run(self):
        while True:
            # Long running task ...
            self.signalExample.emit("leet", 1337)
            time.sleep(5)

class Main(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.worker = WorkerThread()
        self.workerThread = QtCore.QThread()
        self.workerThread.started.connect(self.worker.run)  # Init worker run() at startup (optional)
        self.worker.signalExample.connect(self.signalExample)  # Connect your signals/slots
        self.worker.moveToThread(self.workerThread)  # Move the Worker object to the Thread object
        self.workerThread.start()

    def signalExample(self, text, number):
        print(text)
        print(number)

if __name__== '__main__':
    app = QtWidgets.QApplication([])
    gui = Main()
    sys.exit(app.exec_())
(Mar-02-2019, 06:02 PM)mrdominikku Wrote: [ -> ]3. After push_import click Widow Explorer dialog window pop up, when I choose correct file code is finish properly, but when I want to close the dialog window before choosing file whole program close instead to going back to main GUI. How I should handle with this event?


To avoid this you must add
app.setQuitOnLastWindowClosed(False)
@Alfalfa, thanks for response, 1st and 3rd applied, but still can't make 2nd work. It doesn't crash program or anything, but either emit signal or call class procedure don't update progress bar :( I added part of convert module below.

class Window(QtBaseClass, Ui_MainWindow):

    def __init__(self, parent=None):

        super(QtBaseClass, self).__init__(parent)

        self.setupUi(self)
        self.threading = ThreadClass(self)
        self.threading.signal.connect(self.update_progress_bar)

        self.push_import.clicked.connect(self.import_data)
        self.push_cancel.clicked.connect(self.close_app)
        self.push_ok.clicked.connect(self.process_input)

    def update_progress_bar(self, progress_val):
        self.input_progress_bar.setValue(progress_val)

    def import_data(self):
        db_analysis.insert_data()

    def process_input(self):
        self.threading.start()

    def closeEvent(self, event):
        odp = QMessageBox.question(
            self, 'Exit',
            "Are you sure you want exit?",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

        if odp == QMessageBox.Yes:
            event.accept()
        else:
            event.ignore()

    def close_app(self):
        odp = QMessageBox.question(
            self, 'Exit',
            "Are you sure you want exit?",
            QMessageBox.Yes | QMessageBox.No, QMessageBox.No)

        if odp == QMessageBox.Yes:
            sys.exit()


class ThreadClass(QThread):

    signal = QtCore.pyqtSignal(int)

    def __init__(self, window, parent=None):
        super(ThreadClass, self).__init__(parent)
        self.window = window

    def run(self):
        convert.convert_main(self, self.window.input_line.text())


# convert module

def convert_main(worker, input_array):

    worker = worker.signal
    window = ui_main.Window()

    my_list = list(map(int, input_array.split(" ")))

    for i to my_list:
         #some loop code
         worker.emit(my_list.index(a)/len(my_list))
         window.update_progress_bar(my_list.index(a)/len(my_list))
(Mar-04-2019, 05:50 AM)mrdominikku Wrote: [ -> ]@Alfalfa, thanks for response, 1st and 3rd applied, but still can't make 2nd work. It doesn't crash program or anything, but either emit signal or call class procedure don't update progress bar :( I added part of convert module below.

Your usage of threading, slot and signals is incorrect. Look again at the example I gave you above. If you have trouble understanding how it work, I propose you start with this very short threading example and grow your program around it.
(Mar-04-2019, 11:28 AM)Alfalfa Wrote: [ -> ]
(Mar-04-2019, 05:50 AM)mrdominikku Wrote: [ -> ]@Alfalfa, thanks for response, 1st and 3rd applied, but still can't make 2nd work. It doesn't crash program or anything, but either emit signal or call class procedure don't update progress bar :( I added part of convert module below.

Your usage of threading, slot and signals is incorrect. Look again at the example I gave you above. If you have trouble understanding how it work, I propose you start with this very short threading example and grow your program around it.

Been searching for few solutions and still don't get it. In PyQt5 documentation is described how to use signals and slot, but even after using them I can't call emit signal from another module, but I can emit from same module.

#*******ui_main.py module*******

class Window(QtBaseClass, Ui_MainWindow):

    def __init__(self, parent=None):

        super(QtBaseClass, self).__init__(parent)

        self.setupUi(self)
        self.worker = ThreadClass(self) #<-- thread class initialize
        self.worker.signal.connect(self.update_progress_bar)  # <--connecting slot to signal
        
    def update_progress_bar(self, progress_val):
        self.input_progress_bar.setValue(progress_val)

    def process_input(self):
        self.worker.start()  #<-- starting thread

class ThreadClass(QThread):

    signal = QtCore.pyqtSignal(int) #<-- my signal

    def __init__(self, window, parent=None):
        super(ThreadClass, self).__init__(parent)
        self.window = window

    def run(self):
        self.signal.emit(20)  #<--that's work
        convert.convert_main(self.signal, self.window.input_line.text()) #<--pass signal to another module

#**********convert.py module****

def convert_main(s_thread, input_array):
    s_thread.emit(20) #<-- that's not working
Do not subclass QThread. Instead you must subclass a QObject and place it into a QThread using the moveToThread method.

But I don't get what you are trying to do; signals are meant to be emitted from within the thread, so why would you want to call it from outside? Instead you can simply connect your second thread to the same slot and emit from there.
(Mar-10-2019, 03:12 PM)Alfalfa Wrote: [ -> ]But I don't get what you are trying to do; signals are meant to be emitted from within the thread, so why would you want to call it from outside? Instead you can simply connect your second thread to the same slot and emit from there.

I prefer functional programming in python so I don't like to store all functions in one module. So all GUI related functions should be kept in ui_main.py and all other in convert.py. Convert_main function based on given input (list of 10-20 numbers - self.window.input_line.text()) perform loop for each number from the list. At the end of each loop I want to update progress bar so user will know the progress of the main code. ThreadClass.start() starts thread, so as far def run() is running convert.convert_main() is within thread (at least I understand it like that) so signal.emit() in convert_main should be emitted to main GUI thread.

If I don't find better solution which meets my needs I will need to make less slick coding and put whole convert_main() code withing run() :(
(Mar-10-2019, 03:56 PM)mrdominikku Wrote: [ -> ]I prefer functional programming in python so I don't like to store all functions in one module. So all GUI related functions should be kept in ui_main.py and all other in convert.py.

From your main class, you can init instances of other class and connect their signals and slots from there. This way your main class is kept to a minimum and handle all the connections between the widgets and the logic. For instance;

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

class WorkerThread(QtCore.QObject):
    signal = QtCore.pyqtSignal(int)

    def __init__(self):
        super().__init__()

    @QtCore.pyqtSlot()
    def run(self):
        while True:
            number = random.randint(1, 1000)
            self.signal.emit(number)
            time.sleep(1)

class WorkerLabel(QtWidgets.QLabel):
    def __init__(self, parent):
        super().__init__()

    @QtCore.pyqtSlot(int)
    def slot(self, i):
        self.setText(str(i))

class UserInterface(QtWidgets.QWidget):
    def __init__(self, parent):
        super().__init__()
        self.label = WorkerLabel(self)
        self.layout = QtWidgets.QVBoxLayout()
        self.layout.addWidget(self.label)
        self.setLayout(self.layout)

class Main(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui = UserInterface(self)
        self.setCentralWidget(self.ui)

        self.worker = WorkerThread()
        self.workerThread = QtCore.QThread()  # Move the Worker object to the Thread object
        self.workerThread.started.connect(self.worker.run)  # Init worker run() at startup
        self.worker.moveToThread(self.workerThread)
        self.worker.signal.connect(self.ui.label.slot)
        self.workerThread.start()

        self.show()

if __name__== '__main__':
    app = QtWidgets.QApplication([])
    gui = Main()
    sys.exit(app.exec_())
Here I made a subclass for the label, but if all it does is having one slot, you might put the method into UserInterface and use self.label.setText() instead.
Found solution..

The signal was emitted but the value that is emitted is always almost zero as:

0 < worker.emit(my_list.index(a)/len(my_list)) < 1, where len(my_list) = 11, so 1/11 = 0.09

Added worker.emit(my_list.index(a)/len(my_list)*100)

SORRY if I didn't mentioned that! Thread can be closed :)