Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In PyQt, what is the best way to share data between the main window and a thread

I'm in the process of writing my very first GUI application with PyQt4 and I've come upon a question that seems very basic, yet I don't seem to find a good answer:

I'm using a thread to continuously perform a repeated task without blocking the main window. The thread needs some information from the main window (e.g. the current value of a spinbox) that can also change during the runtime of the thread. In this situation, what is the proper way to share such data between the main window and the thread?

Naively, I could come up with the following possibilities:

  1. Pass a reference to the host window to the thread and use this to retrieve the current value of the variable in question (see example below).
  2. Keep a copy of the variable in the thread and keep it synchronized with the main window by emitting signals whenever it changes.
  3. Use global variables.

All three options would most likely work for my particular use case (though 2 would be a bit complicated), but I have a feeling there should be a better/more Pythonic/more Qt-like way.

Here is a minimum working example illustrating what I want to do, in this case using option 1:

from PyQt4 import QtGui, QtCore
import time, sys

class MainWindow(QtGui.QWidget):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.layout = QtGui.QVBoxLayout(self)
        self.spinbox = QtGui.QSpinBox(self)
        self.spinbox.setValue(1)
        self.layout.addWidget(self.spinbox)
        self.output = QtGui.QLCDNumber(self)
        self.layout.addWidget(self.output)

        self.worker = Worker(self)
        self.connect(self.worker, QtCore.SIGNAL('beep'), self.update)
        self.worker.start()

    def update(self, number):
        self.output.display(number)


class Worker(QtCore.QThread):
    def __init__(self, host_window):
        super(Worker, self).__init__()
        self.host = host_window
        self.running = False

    def run(self):
        self.running = True
        i = 0
        while self.running:
            i += 1
            self.emit(QtCore.SIGNAL('beep'), i)
            sleep_time = self.host.spinbox.value()
            time.sleep(sleep_time)

    def stop(self):
        self.running = False


app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

PS: Since I'm completely unexperienced with PyQt it's not unlikely that there are other problems with the code or the question is unclear. In this case, please feel free to comment or edit the question.

like image 703
Emil Avatar asked Oct 12 '15 12:10

Emil


1 Answers

Widgets are not thread safe, see Threads and QObjects:

Although QObject is reentrant, the GUI classes, notably QWidget and all its subclasses, are not reentrant. They can only be used from the main thread.

And see more definitions here: Reentrancy and Thread-Safety


You should only use widgets in the main thread, and use signal and slots to communicate with other threads.

I don't think a global variable would work, but I honestly don't know why.


How to use signals in this example:

#in main
self.worker = Worker(self.spinbox.value())
self.worker.beep.connect(self.update)
self.spinbox.valueChanged.connect(self.worker.update_value)

class Worker(QtCore.QThread):
    beep=QtCore.pyqtSignal(int)

    def __init__(self,sleep_time):
        super(Worker, self).__init__()
        self.running = False
        self.sleep_time=sleep_time

    def run(self):
        self.running = True
        i = 0
        while self.running:
            i += 1
            self.beep.emit(i)
            time.sleep(self.sleep_time)

    def stop(self):
        self.running = False

    def update_value(self,value):
        self.sleep_time=value

NB: I use the new style signal and slots

like image 75
Mel Avatar answered Sep 18 '22 12:09

Mel