0

Hi im trying to understand threading in pyqt5. Iv'e outlined the process i use currently. Is this the correct way or is there a better way? Note, i am a complete threading noob.

This is how i pass data from GUI object. For example a combobox has changed, i have a self.comboBox.activated.connect(comboBoxChanged) in GUI class and then that corresponding function sets SharedVars.comboBoxChanged = True and then in one of the threadloops i check and see if SharedVars.comboBoxChanged == True and if it is do self.updateOtherStuffWhenComboBoxChanged.emit(getData()) and SharedVars.comboBoxChanged = False

class SharedVars():
    ...

#This is my GUI class
class Ui_MainWindow(QThread, SharedVars):
    def setupUi(self, MainWindow):
        #self.table stuff

#this is the class i start threads, get a response and update the GUI
class MainUIClass(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__()
        self.setupUi(self)
        self.threadclass = ThreadClass()
        self.threadclass.start()
        self.threadclass.updateTable.connect(self.updateTable)
        self.threadclass2 = ThreadClass2()
        self.threadclass2.start()
        self.threadclass2.etc.connect(self.etc)


    def updateTable(self, val):
         #print("update self.table stuff")

    def etc(self, val):
        #print("update other stuff on gui")

#my first thread. If i have network calls that block i use several threads
class ThreadClass(QtCore.QThread, SharedVars):
    updateTable = pyqtSignal(list)
    def __init__(self, parent=None):
        super().__init__()

    def run(self):
        def getData():
            ...
            return [data]
        while True:
            self.updateTable.emit(getData())

#my second thread
class ThreadClass2(QtCore.QThread, SharedVars):
    etc = pyqtSignal(list)
    def __init__(self, parent=None):
        super().__init__()

    def run(self):
        def getData():
            ...
            return [data]
    while True:
        self.etc.emit(getData())

if __name__ == "__main__":
    a = QtWidgets.QApplication(sys.argv)
    app = MainUIClass()

    app.show()

    sys.exit(a.exec_())

Perhaps there is a better way. Perhaps i can send a signal directly from GUI object to threadclass for example.

Edit: Ok, so "three_pineapples" showed me an example which i converted to qt5, changed some terminology and otherwise changed a bit. I removed the decorators too because i couldnt get them to work with qt5, i don't know if this was a bad idea(although it still seems to work):

from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtCore import pyqtSignal, QThread, QTimer

import time
import sys

class MyTask(QtCore.QObject):
    disableBtn = pyqtSignal()
    enableBtn = pyqtSignal()

    def firstTask(self):
        self.disableBtn.emit()
        print('oooh so much processing...')
        time.sleep(4)
        print('phew, finished!')
        self.enableBtn.emit()

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

        self.initUi()
        self.setupThread()

    def initUi(self):
        layout = QtWidgets.QVBoxLayout()
        self.button = QtWidgets.QPushButton('Click Me')
        layout.addWidget(self.button)
        self.setLayout(layout)
        self.show()

    def enableBtn(self):
        self.button.setEnabled(True)

    def disableBtn(self):
        self.button.setEnabled(False)

    def setupThread(self):
        self.thread = QThread()
        self.task = MyTask()

        self.task.moveToThread(self.thread)

        #self.thread.started.connect(self.task.firstTask)
        self.button.clicked.connect(self.task.firstTask)
        self.task.disableBtn.connect(self.disableBtn)
        self.task.enableBtn.connect(self.enableBtn)

        # Start thread
        self.thread.start()    

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    w = Window()
    app.exec_()

Someone talked something about memory problems in the link you posted. I will be frequently updating 3 table elements from DB, is this something i need to be worried about? Also, the usage of time.sleep(). I hear one is not supposed to use time.sleep() with threading? Maybe i can use QThread.msleep instead?

n00p
  • 269
  • 4
  • 11

1 Answers1

0

I'm not sure if the threading is correct, but I am pretty confident that it is not needed. One of Qt's main strengths is that it is event-based. The programmer connects signals and slots among the various components to define how you want the application to behave when certain events happen.

In the code you've posted, you're running these background threads, whose sole job appears to be to act as a middleman between the events in the GUI (button clicks) and other logic (update some table, make a network request). Just connect the signal emitted when the button is clicked directly to functions which do what you want.

That is you have:

clicked --> thread makes network request --> thread emits signal with reply -->
    GUI receives data from thread signals and does some more work.

It would be much simpler to do something like:

clicked --> make network request --> respond to network reply

All of this can happen in the main thread, and Qt will do all the network I/O in an asynchronous way so that the rest of the application (e.g., GUI) is not blocked.

For example, you could update your main window to include two slots which make the network requests and handles the responses, and connect those directly to the button-click signal. So, it might look something like this:

#!/usr/bin/env python3

import sys
from PyQt5 import QtWidgets, QtCore, QtNetwork

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.nam = QtNetwork.QNetworkAccessManager(self)
        self.push_button = QtWidgets.QPushButton("Make request", self)
        self.setCentralWidget(self.push_button)

        # When button is pressed, make network request
        self.push_button.clicked.connect(self.make_request)

        # When reply is received, do "work".
        self.nam.finished.connect(self.handle_reply)

    QtCore.pyqtSlot()
    def make_request(self):
        request = QtNetwork.QNetworkRequest(QtCore.QUrl("http://www.google.com"))
        self.nam.head(request)

    QtCore.pyqtSlot(QtNetwork.QNetworkReply)
    def handle_reply(self, reply):
        # This just prints the status code, but you can have
        # this slot update the GUI, change some data structure,
        # or emit another signal.
        print('Status code:',
                  reply.attribute(QtNetwork.QNetworkRequest.HttpStatusCodeAttribute))
        reply.deleteLater()

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

This is all assuming you're making HTTP requests. If you're using a lower-level protocol over something like TCP, you'd need a QtNetwork.QTcpSocket, and you'd use the QTcpSocket.readyRead() signal instead of the QNetworkAccessManager.finished() signal.

bnaecker
  • 6,152
  • 1
  • 20
  • 33
  • I can't use this approach because i need to do a lot of processing on the data before i send the data to the table for displaying. I am fairly certain that the UI will freeze if i do that processing in the GUI thread. For light processing it seems to not be noticable, but for heavy it freezes. I am almost 100% on that. I also tested now with a time.sleep(10) in the gui upon receiving data from thread and it hangs the GUI so i think it is correct. There's a further problem with using QNetworkRequest because i rely on an external lib to do my network calls which i have to use. – n00p Jan 07 '18 at 05:40
  • @n00p Yes, doing heavy work in the main thread is not a good idea. The approach in your edited question is OK, using a dedicated "task" object which lives in another thread for the I/O and/or processing. – bnaecker Jan 07 '18 at 17:49