0

I have a GUI which needs to perform work that takes some time and I want to show the progress of this work, similar to the following:

import sys
import time
from PyQt4 import QtGui, QtCore

class MyProgress(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        # start loop with signal
        self.button = QtGui.QPushButton('loop', self)
        self.connect(self.button, QtCore.SIGNAL('clicked()'), self.loop)

        # test button
        self.test_button = QtGui.QPushButton('test')
        self.connect(self.test_button, QtCore.SIGNAL('clicked()'), self.test)

        self.pbar = QtGui.QProgressBar(self)
        self.pbar.setMinimum(0)
        self.pbar.setMaximum(100)

        # layout
        vbox = QtGui.QVBoxLayout()
        vbox.addWidget(self.test_button)
        vbox.addWidget(self.button)
        vbox.addWidget(self.pbar)
        self.setLayout(vbox)

        self.show()

    def update(self):
        self.pbar.setValue(self.pbar.value() + 1)

    def loop(self):
        for step in range(100):
            self.update()
            print step
            time.sleep(1)

    def test(self):
        if self.test_button.text() == 'test':
            self.test_button.setText('ok')
        else:
            self.test_button.setText('test')

app = QtGui.QApplication(sys.argv)
view = MyProgress()
view.loop()  # call loop directly to check whether view is displayed
sys.exit(app.exec_())

When I execute the code the loop method is called and it prints out the values as well as updates the progress bar. However the view widget will be blocked during the execution of loop and although this is fine for my application it doesn't look nice with Ubuntu. So I decided to move the work to a separate thread like this:

import sys
import time
from PyQt4 import QtGui, QtCore

class Worker(QtCore.QObject):
    def __init__(self, parent=None):
        QtCore.QObject.__init__(self, parent)

    def loop(self):
        for step in range(10):
            print step
            time.sleep(1)

class MyProgress(QtGui.QWidget):
    def __init__(self, parent=None):
        QtGui.QWidget.__init__(self, parent)

        # test button
        self.test_button = QtGui.QPushButton('test')
        self.connect(self.test_button, QtCore.SIGNAL('clicked()'), self.test)

        self.pbar = QtGui.QProgressBar(self)
        self.pbar.setMinimum(0)
        self.pbar.setMaximum(100)

        # layout
        vbox = QtGui.QVBoxLayout()
        vbox.addWidget(self.test_button)
        vbox.addWidget(self.pbar)
        self.setLayout(vbox)

        self.show()

    def test(self):
        if self.test_button.text() == 'test':
            self.test_button.setText('ok')
        else:
            self.test_button.setText('test')

app = QtGui.QApplication(sys.argv)

view = MyProgress()
work = Worker()

thread = QtCore.QThread()
work.moveToThread(thread)
# app.connect(thread, QtCore.SIGNAL('started()'), work.loop)  # alternative
thread.start()
work.loop()  # not called if thread started() connected to loop

sys.exit(app.exec_())

When I run this version of the script the loop starts running (the steps are displayed in the terminal) but the view widget is not shown. This is the first thing I can't quite follow. Because the only difference from the previous version here is that the loop runs in a different object however the view widget is created before and therefore should be shown (as it was the case for the previous script).

However when I connected the signal started() from thread to the loop function of worker then loop is never executed although I start the thread (in this case I didn't call loop on worker). On the other hand view is shown which makes me think that it depends whether app.exec_() is called or not. However in the 1st version of the script where loop was called on view it showed the widget although it couldn't reach app.exec_().

Does anyone know what happens here and can explain how to execute loop (in a separate thread) without freezing view?

EDIT: If I add a thread.finished.connect(app.exit) the application exits immediately without executing loop. I checked out the 2nd version of this answer which is basically the same what I do. But in both cases it finishes the job immediately without executing the desired method and I can't really spot why.

Community
  • 1
  • 1
a_guest
  • 34,165
  • 12
  • 64
  • 118

1 Answers1

0

The example doesn't work because communication between the worker thread and the GUI thread is all one way.

Cross-thread commnunication is usually done with signals, because it is an easy way to ensure that everything is done asynchronously and in a thread-safe manner. Qt does this by wrapping the signals as events, which means that an event-loop must be running for everything to work properly.

To fix your example, use the thread's started signal to tell the worker to start working, and then periodically emit a custom signal from the worker to tell the GUI to update the progress bar:

class Worker(QtCore.QObject):
    valueChanged = QtCore.pyqtSignal(int)

    def loop(self):
        for step in range(0, 10):
            print step
            time.sleep(1)
            self.valueChanged.emit((step + 1) * 10)
...

thread = QtCore.QThread()
work.moveToThread(thread)
thread.started.connect(work.loop)
work.valueChanged.connect(view.pbar.setValue)
thread.start()

sys.exit(app.exec_())
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • I know the example was only one way but the problem was that the `started` signal didn't trigger `worker.loop` although I connected them. But that must've been a OS problem (I created the example on some RedHat derivative) cause when I tried it on my Ubuntu it worked fine. Thus I think the question is redundant. – a_guest Mar 02 '15 at 21:19
  • @a_guest. No, your second example wouldn't work anyway - it must have the changes I suggested to work properly. – ekhumoro Mar 02 '15 at 21:45
  • It works, you just have to comment `work.loop` and uncomment `connect(...)` what I already indicated in my code. I was just puzzled by the fact that the widget `view` is shown when you call `loop` on `view` (as in my first example) but it is not shown when you create `view` separately and then call `loop` on `work`. – a_guest Mar 03 '15 at 08:24
  • @a_guest. It clearly does not work: the progress bar is not updated. Your question asked how to make a non-blocking version of the first example, and to explain the interaction between threads, signals and the event-loop, which my answer did. I don't understand why you are being so argumentative about this. – ekhumoro Mar 03 '15 at 17:18
  • My purpose was not an updating progr. bar but a GUI that doesn't freeze. That's why I can came up with my second example where I did basically the same what you did in you answer (I didn't update the progress bar because it was not shown in the first place and **that** was what puzzled me). If only about how to move tasks to different threads this question is a duplicate and not very informative. However if you know some stuff about PyQt4's signals and slots maybe you can answer me this question: http://stackoverflow.com/questions/28829553/is-it-safe-to-disconnect-a-slot-before-it-was-called – a_guest Mar 03 '15 at 22:35