2

I am writing an application that uses a Qt GUI and am now trying to multithread it (first time learner). A longer task will be contained within the worker thread eventually but output logs will be required to be written as it goes. The GUI class has a method to output these logs to a plain text widget. Up until now, everything has been running within the GUI class.

I currently have the following code (included the important bits for brevity):

class Worker(QRunnable):
    @pyqtSlot()
    def run(self):
        Ui.logentry(self, "Test")
    
class Ui(QtWidgets.QMainWindow):
    def __init__(self):
        super(Ui, self).__init__()
        uic.loadUi(resourcepath('./pycdra.ui'), self)
        
        self.outputlog = self.findChild(QtWidgets.QPlainTextEdit, 'outputlog')
        self.button = self.findChild(QtWidgets.QPushButton, 'button')
        self.button.clicked.connect(self.Button)
        self.threadpool = QThreadPool()
        self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())
        
    def Button(self):
        worker = Worker()
        self.threadpool.start(worker)
    
    def logentry(self, returntext):
        self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))
        self.outputlog.repaint()
        
    def timestamp(self):
        import datetime
        ts = datetime.datetime.now(tz=None).strftime("%Y-%m-%d %H:%M:%S"))
        return ts
        
def applic():
    app = QApplication(sys.argv)
    window = Ui()
    window.show()
    sys.exit(app.exec_())

applic()

When I try running this, the GUI loads perfectly, but upon pushing button the Ui.logentry part of Worker returns the error: AttributeError: 'Worker' object has no attribute 'outputlog'

I attempted making logentry() and timestamp() global so they can access and be accessed by both classes, but the closest I got with the line Ui.self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext)) was that 'Ui' object has no attribute 'self'.

What am I missing? Am I right in making them global or are there other means to do this?

Chokehold
  • 67
  • 7
  • Something might have changed in the meantime, but at least on Python 3.7 the above code results in a syntax error right from the first decorator. And, in any case, the `pyqtSlot` decorator only works on QObject subclasses, which QRunnable is not. – musicamante May 01 '21 at 02:49
  • I'm trying to answer you, but as I'm going through your code I find more and more issues. Please, ensure that you're providing a reliable code, which should be both [minimal and reproducible](https://stackoverflow.com/help/minimal-reproducible-example). – musicamante May 01 '21 at 03:00
  • Oops, that's on me. The code is on a closed off network so I had to retype it by hand and forgot `def run(self):` in the Worker class. Edited question to include it. Regarding the decorator, it was in a tutorial example I was following and I thought it was important. If it's not, it's gone. – Chokehold May 01 '21 at 03:01
  • Don't remove portions that are part of your issue. Just ensure that your code correctly and exactly reproduces it. – musicamante May 01 '21 at 03:03

2 Answers2

3

There are various problems with your code. I'll try to address them in order of importance.

  1. logentry is an instance method of Ui, which means that it requires an instance of Ui in order to be run; what you're trying to achieve would never work since that self refers to an instance of Worker, which clearly doesn't have any outputlog attribute (which is what logentry expects instead). Remember: the self argument is not there just for fun, it always refers to the object (the instance, for instance methods) the function is called from; self.logentry means "call the function logentry using self as first argument. Since, in your case, self refers to an instance of Ui, that explains your main issue, since that instance doesn't have that attribute.

  2. None is exactly what its name says: nothing. The timestamp function will throw another AttributeError, since there's no strftime attribute in None. That is part of a datetime object, so you have to get a now object, then call its strftime function against it. In any case, the tz argument requires a tzinfo subclass.

  3. pyqtSlot only works for Qt classes that inherit from QObject (such as QWidget). QRunnable isn't one of those classes. You can check the whole inheritance tree in the header of the documentation in each class: if there's a "Inherits:" field, go up until there's none. If you get a QObject inheritance at some point, then you can use slots and signals for that object, otherwise not. Consider that: QWidget also inherits from QObject; all Qt widgets inherit from QWidget; there are Qt classes that inherit from QObject but are not widgets.

  4. Even ignoring all the above, access to UI elements is always forbidden from external threads (including QRunnable objects); while you can theoretically (but unreliably) get their properties, trying to set them will most likely cause a crash; in order to change something in a UI element, using signals is mandatory. Note that "access" also includes creation, and always results in a crash.

  5. Calling repaint is a common (and wrong) attempt to solve the above issue; that's unnecessary, as properly setting widget properties (like using appendPlainText()) already results in a scheduled repaint on its own; there are very few cases for which repaint is actually necessary, and the rule of thumb is that if you're calling it you probably don't know what your doing or why your doing it. In any case, calling update() is always preferred, and it must always be called from the main UI thread anyway.

  6. Using imports in a function is rarely required, as import statements should always be in the very beginning of the script; while there are cases for which imports can (or should) be done later or in a specific function, doing them in a function that is likely to be called often, makes using them there completely pointless. Also, datetime is part of the standard library, so importing it on demand will hardly affect performance (especially considering its "performance weight" against what a big library like Qt is compared to it).

  7. When the ui is loaded from a .ui file (or a pyuic generated file), PyQt alread creates all widgets as instance attributes, so there's no need for findChild. Unfortunately there are a lot of tutorials that suggest that approach, and they are just completely and plain wrong. You can already access those widgets as self.outputlog and self.button right after uic.loadUi.

  8. Function names (like variables and attributes) should always begin with a lower case letter, as only classes and constants should begin with upper cases (see the official Style Guide for Python Code). Also, object names should always explain what those object do (see "self-documenting code"): a function that does an "action" should have a verb; if it's named "Button" it doesn't tell me that it's going to do some processing, and that's not a very good thing.

  9. A "main" function (like your applic) usually makes sense within the common if __name__ == '__main__': block, which ensures that that function doesn't get called in case the file gets imported instead of being directly run. (See this answer and the related question).


Since QRunnable doesn't inherit from QObject, we can create a QObject subclass that acts as a signal "proxy", and make it a member of the QRunnable instance. Then we must connect to that signal everytime we create a new Worker object.

Here is a revised version of your code, based on the above points.

import datetime
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi

class WorkerSignals(QObject):
    mySignal = pyqtSignal(str)


class Worker(QRunnable):
    def __init__(self):
        super().__init__()
        self.signalProxy = WorkerSignals()
        self.mySignal = self.signalProxy.mySignal

    def run(self):
        self.mySignal.emit("Test")


class Ui(QMainWindow):
    def __init__(self):
        super(Ui, self).__init__()
        loadUi('./pycdra.ui', self)
        
        self.button.clicked.connect(self.startWorker)
        
        self.threadpool = QThreadPool()
        self.logentry("Available threads: %d" % self.threadpool.maxThreadCount())

    def startWorker(self):
        worker = Worker()
        worker.mySignal.connect(self.logentry)
        self.threadpool.start(worker)

    def logentry(self, returntext):
        self.outputlog.appendPlainText(self.timestamp() + " " + str(returntext))

    def timestamp(self):
        ts = datetime.datetime.now()
        return ts.strftime("%Y-%m-%d %H:%M:%S")


def applic():
    import sys
    app = QApplication(sys.argv)
    window = Ui()
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    applic()

I suggest you to carefully study the provided code and the differences with yours, and then do some careful, patient research both on the links given above and the following topics:

  • classes and instances, and the meaning of self;
  • python types (including None);
  • what is event driven programming and how it relates with graphical interfaces;
  • the most important Qt classes, QObject and QWidget, and all their properties and functions (yes, they are a lot);
  • general PyQt related topics (most importantly, signals/slots and properties);
  • code styling and good practices;
musicamante
  • 41,230
  • 6
  • 33
  • 58
0

Use the class' initialised form

class foo:pass
selfuotsideclass=foo()

Use variable selfoutsideclass as self

Supergamer
  • 411
  • 4
  • 13