0

I'm trying to write a pyqt5 application with a long running, but not CPU intensive process. I'd like to be able to run it without hanging the UI, so I'm trying to use threading, but since it doesn't seem like I can just run a thread and have it stop after its gone through its code so that it can be run again, I've tried setting up the thread to wait for a variable to change before running.

I know this can't be the correct pattern for running long processes in a pyqt app.

import time
import threading
from PyQt5 import QtWidgets, uic


class MyApp(QtWidgets.QMainWindow):
    _run_thread = False

    def __init__(self):
        QtWidgets.QMainWindow.__init__(self)
        self.ui = uic.loadUi('myapp.ui', self)

        self.ui.start_thread_button.clicked.connect(self._run_thread_function)

        self._thread = threading.Thread(target=self._run_thread_callback)
        self._thread.daemon = True
        self._thread.start()

        self.ui.show()

    def _run_thread_callback(self):
        while True:
            if self._run_thread:
                print("running thread code...")
                time.sleep(10)
                print("thread code finished")
                self._run_thread = False

    def _run_thread_function(self):
        print("starting thread...")
        self._run_thread = True


def main():
    app = QtWidgets.QApplication(sys.argv)
    MyApp()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
waspinator
  • 6,464
  • 11
  • 52
  • 78
  • Well, you're going to run into problems because you have a method called `self._run_thread` and an instance attribute with the same name. At some point you're overriding your method with a Boolean. But regardless of that, you should probably switch to QThreads. Have a look at http://stackoverflow.com/documentation/pyqt/2775/using-threads-with-pyqt#t=20161111002348120811 which should be easily adaptable to PyQt5. There are also some guidelines about thread safety at the bottom of the page. – three_pineapples Nov 11 '16 at 00:25
  • okay, I've changed the name of the function, but that was really just an example, and not the main point of my question. So from what I'm hearing the native python `threading` module isn't the right thing to use with a Qt. Is that true for any UI application? For example if I switched from Qt to Tkinter, could I then use the `threading` module? – waspinator Nov 11 '16 at 00:39
  • I use the `threading` module with Qt successfully, but you're not supposed to ([see here](http://stackoverflow.com/q/1595649/1994235)). If you want to interact with the GUI from your thread, you really should use QThreads and signals/slots (as direct access to GUI objects from a thread is forbidden). However, if you really want to use python threads, it seems safe to use `QApplication.postEvent` to communicate between a secondary thread and the main thread. I have a project which is based on this idea [here](https://bitbucket.org/philipstarkey/qtutils/wiki/invoke_in_main) (currently PyQt4 only – three_pineapples Nov 11 '16 at 00:52

1 Answers1

1

Below is a simple demo showing how to start and stop a worker thread, and safely comminucate with the gui thread.

import sys
from PyQt5 import QtCore, QtWidgets

class Worker(QtCore.QThread):
    dataSent = QtCore.pyqtSignal(dict)

    def __init__(self, parent=None):
        super(Worker, self).__init__(parent)
        self._stopped = True
        self._mutex = QtCore.QMutex()

    def stop(self):
        self._mutex.lock()
        self._stopped = True
        self._mutex.unlock()

    def run(self):
        self._stopped = False
        for count in range(10):
            if self._stopped:
                break
            self.sleep(1)
            data = {
                'message':'running %d [%d]' % (
                    count, QtCore.QThread.currentThreadId()),
                'time': QtCore.QTime.currentTime(),
                'items': [1, 2, 3],
                }
            self.dataSent.emit(data)

class Window(QtWidgets.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.edit = QtWidgets.QPlainTextEdit()
        self.edit.setReadOnly(True)
        self.button = QtWidgets.QPushButton('Start')
        self.button.clicked.connect(self.handleButton)
        layout = QtWidgets.QVBoxLayout(self)
        layout.addWidget(self.edit)
        layout.addWidget(self.button)
        self._worker = Worker()
        self._worker.started.connect(self.handleThreadStarted)
        self._worker.finished.connect(self.handleThreadFinished)
        self._worker.dataSent.connect(self.handleDataSent)

    def handleThreadStarted(self):
        self.edit.clear()
        self.button.setText('Stop')
        self.edit.appendPlainText('started')

    def handleThreadFinished(self):
        self.button.setText('Start')
        self.edit.appendPlainText('stopped')

    def handleDataSent(self, data):
        self.edit.appendPlainText('message [%d]' %
            QtCore.QThread.currentThreadId())
        self.edit.appendPlainText(data['message'])
        self.edit.appendPlainText(data['time'].toString())
        self.edit.appendPlainText(repr(data['items']))

    def handleButton(self):
        if self._worker.isRunning():
            self._worker.stop()
        else:
            self._worker.start()

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(500, 100, 400, 400)
    window.show()
    sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • Is it possible to directly manipulate UI elements from within the `Worker`? I'd like to manipulate several elements from a thread. Would I have to send a json string from the thread and then parse it in the window before changing the UI? – waspinator Nov 17 '16 at 15:23
  • 1
    @waspinator. Directly manipulating the gui outside of the main thread is not thread-safe. You don't need to use json: it's okay to send an ordinary python `dict`, which can contain other complex types (but obviously not gui elements). I've updated my example so that the worker sends a `dict` back to the main thread. – ekhumoro Nov 17 '16 at 17:15