0

Today I came across an interesting situation when a line below is executed before the line proceeding it (line above). A simple version of the situation is posted below. A brief description: There are three QGroupBox() connected to the same window: groupboxA, groupboxB and groupboxC. Groupbox B is hidden. Clicking the Ok button unhides a second groupBox B and hides first groupBox A. A repetitive clicking continues this sequence of hide/unhide events.

The very last line is calling calcA() method processing of which takes around 2-3 seconds to compete. An issue: Even while a call for calcA() is on a last line it is being executed before QGroupBoxes visibilities are changed for which the code written above is responsible. I would like to know why it is happening and how to get around it.

enter image description here

from PyQt4 import QtGui, QtCore

class MyApp(object):

    def __init__(self):
        super(MyApp, self).__init__()

        app = QtGui.QApplication(sys.argv)
        self.mainWidget = QtGui.QWidget()
        self.mainLayout = QtGui.QVBoxLayout()
        self.mainWidget.setLayout(self.mainLayout)

        # A
        self.groupboxA = QtGui.QGroupBox()
        self.layoutA = QtGui.QVBoxLayout()
        self.groupboxA.setLayout(self.layoutA)

        lineA = QtGui.QTextEdit('This is QGroupBox A')
        self.layoutA.addWidget(lineA) 

        # B
        self.groupboxB = QtGui.QGroupBox()
        self.layoutB = QtGui.QVBoxLayout()
        self.groupboxB.setLayout(self.layoutB)
        self.groupboxB.setHidden(True)

        labelB = QtGui.QLabel('This is QGroupBox B')
        self.layoutB.addWidget(labelB) 

        # C
        self.groupboxC = QtGui.QGroupBox()
        self.layoutC = QtGui.QVBoxLayout()
        self.groupboxC.setLayout(self.layoutC)

        okButton = QtGui.QPushButton('OK')
        okButton.clicked.connect(self.OK)      
        self.layoutC.addWidget(okButton) 


        self.mainLayout.addWidget(self.groupboxA)
        self.mainLayout.addWidget(self.groupboxB)
        self.mainLayout.addWidget(self.groupboxC)
        self.mainWidget.show()
        sys.exit(app.exec_())

    def calcA(arg=None):
        print "Task started..."
        for i in range(5000000):
            pass
        print '...task completed.'


    def OK(self):
        if self.groupboxA.isHidden(): self.groupboxA.setHidden(False)
        else: self.groupboxA.setHidden(True)
        if self.groupboxB.isHidden(): self.groupboxB.setHidden(False)
        else: self.groupboxB.setHidden(True)
        self.calcA()

if __name__ == '__main__':
    MyApp()
3
  • I am not sure it is a good idea to add the ok button to the "C" group box. i+=1 could be replaced by pass. Commented May 3, 2014 at 6:40
  • Also, I would personally use QStackWidget for this, so why are you not doing that? Commented May 3, 2014 at 6:46
  • I've never paid attention to QStackedWidget. I should give it a try. Thanks for pointing out! Commented May 3, 2014 at 18:12

2 Answers 2

2

The updating of the GUI doesn't happen until control is returned to the Qt event loop (once your slot OK finishes).

This is a common issue when you have a long running slot and want GUI changes to be visible before the end of the slot. This SO question has some answers which detail some workarounds: Label in PyQt4 GUI not updating with every loop of FOR loop

Basically, offloading the long running work to a QThread is the best idea in general, but you can only do that easily if your long running code doesn't directly interact with the GUI (you cannot interact with the GUI from a thread, so it gets tricky). Threading is very easy to get wrong, and can badly mess up the stability of your program, so you may not want to go down this road.

Another option is calling QApplication.instance().processEvents() (or assign app to self.app in your __init__ method and then call self.app.processEvents()) immediately after the calls which update the GUI (like setHidden()), but I generally try to avoid this as I consider it bad practice.

The final solution, and one which might work best in your case, would be to call QTimer.singleShot(1,self.calcA) instead of self.calcA. This would return control to the event loop, which would update your GUI and then call self.calcA.

Sign up to request clarification or add additional context in comments.

2 Comments

The singleShot approach still makes the UI block and render it unresponsive, just after the updating (slightly better, but still bad). The proper solution would be to do the calculation in another thread.
I've just tested both solutions outlined by three_pineapples. self.app.processEvents() works as expected well. But 'QTimer.singleShot(1,self.calcA)' is lagging behind and makes almost zero difference (there is still some improvement). Thanks for the help!
1

The issue is you have this, in pseudocode:

def OK(self):
    modify GUI
    self.long_running_function()

The subtlety is that the modifications made to GUI will only get "processed" once the Qt event loop resumes, namely after OK() returns. That's why it appears as though your long running function is executed before the lines above it. It is not, but the lines above it only get "activate" once OK() returns, which is after your long running function has completed.

So you can't have a long-running-function in OK if you want the GUI updates made just prior to be visible. THe solution depends on your long running function: if it is a loop that you can write like this:

def long_running_function(self):
    while condition: 
       self.doQuickStuff()

then you can replace with this:

def doQuickStuff(self):
    if condition:
        .... do stuff ....
        # repeat:
        # 10 ms should give time for some event processing
        QTimer.singleShot(10, self.doQuickStuff) 

def OK(self):
    modify GUI
    doQuickStuff()

Otherwise just move your long running function to a QThread. You'll have to think of following issues:

  1. need to update user that a computation / operation is in progress, result will be available later
  2. might want to update user on actual progress made at regular intervals
  3. You need a way to interrupt the computation in case user quits app while the thread is still running

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.