Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

ScrollArea unresponsive after starting heavy task, unless all widgets have been scrolled into view



  • Hello!

    I have encountered a problem when using a ScrollArea, causing it to freeze if you initiate a heavy task in a separate thread. However, if you manually scroll through the Scroll Area, from top to bottom, a couple of times prior to initiating the heavy task - the error does not occur. I have constructed a small demo:

    # In the MainThread, the GUI and ScrollArea lives.
    # I have a separate QThread called TaskRunner, from which I manage the "heavy" tasks.
    # Starting the Task will spawn a new Thread, in which said task completes its work.
    # I have a QLabel labeled START - Upon pressing this label, it sends a signal to TaskRunner to start all the tasks.
    

    The code can be found here.

    What blows my mind is the following behaviour, I can't seem to find a logical explanation backed by the documentation :

    ALT 1:

    • Start Application --> Press Start Label --> Scrolling Freezes, basically unusable until the tasks (which are running in a separate thread) have finished

    ALT 2:

    • Start Application --> Manually scroll from top to bottom a couple of times --> Press Start Label --> Scrolling works smoothly, no freezes, lag or blocking

    I have attatched the following videos in order to better illustrate: ALT 1 and ALT 2.

    There has to be some form of loading taking place when you scroll the labels into view, but why does it freeze in ALT 1? I have tried to replicate the "Scrolling from top to bottom" by programatically changing the value of the ScrollBar - did not fix anything. I have also tried using ensureWidgetVisible() and blockSignals(True), sadly to no avail.

    So my question is, what is happening when you manually scroll through the entire area? Why does this make the ScrollArea responsive after initiating the heavy task? (Like in ALT 2). And finally, how can I solve this issue, without having to undergo the ritual of manually scrolling from top to bottom before starting the tasks?

    I am very new to PyQT5, is my implementation of threads and Qthreads in the example attatched correct? Or does it perhaps have some major flaw causing this, seemingly, very odd problem?

    Thank You!



  • please provide a Minimal reproducible example


  • Banned

    All right well several things -- 1st when using Qt you should only use QThread and you should definitely not mix QThread with Pythons threading as using Pythons threading within a Qt Application can have adverse extremely hard to trouble-shoot effects. Perhaps like what you are having. Luckily though you are using QThread as it is supposed to be used within Qt5 so that is a plus.

    Now here is a MUC that you can play with. Its a modified version of something I had used as an example for my students and other than having a minor issue with the killing the Timers upon closing the Application (which I have had to deal with before but did not have time to fix) it works and demonstrates how to use QTimers efficiently and without stepping on each other and/or blocking one another.

    from PyQt5.QtCore    import Qt, pyqtSignal, pyqtSlot
    from PyQt5.QtCore    import QCoreApplication, QObject, QThread, QTimer
    from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QScrollArea 
    from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QSlider
    from PyQt5.QtWidgets import QLineEdit, QPushButton, QComboBox, QTextEdit
    
    import sys
    from time import sleep as tmSleep
     
    sys._excepthook = sys.excepthook
    def exception_hook(exctype, value, traceback):
        print(exctype, value, traceback)
        sys._excepthook(exctype, value, traceback)
        sys.exit(1)
    sys.excepthook = exception_hook
     
    class Task(QObject):
        sigOutCur = pyqtSignal(int)
        sigOutTmr = pyqtSignal(int)
      # Initialization after Signals declared
        def __init__(self, CountBy):
            QObject.__init__(self)
            self.CntBy = CountBy
            self.Running = True
            self.Delay = 2
            self.TmrSet = False
            self.TimrOn = False
            self.TmrObj = QObject()
            self.TmrCnt = 0
    
        def StartTask(self):
            CurCnt = 0
            if self.CntBy == 0:
                self.Running = False
            while self.Running:   # Basically always True until ShutDown called
                CurCnt += 1 * self.CntBy
                self.sigOutCur.emit(CurCnt)
              # If you do not give control to the Event Handler with a continuous
              # running process like this it does not matter if it is in its own
              # thread or not as due to rules of the GIL it will lock up the Event
              # Handler causing everything else to "Freeze" up especially in 
              # regards to sending any signals into the Thread
                QCoreApplication.processEvents()
              # Using this to give time for things to occur in the GUI
                tmSleep(self.Delay)
    
        @pyqtSlot(int)
        def ReCountr(self, NewCntBy):
            if NewCntBy == 0:
                self.Running = False
            else:
                self.CntBy = NewCntBy
                if not self.Running:
                    self.Running = True
                    self.StartTask()
    
        @pyqtSlot()
        def TogglTimr(self):
            print('***** Timer Toggle')
            if not self.TmrSet:
                self.TmrSet = True
                self.TmrObj = QTimer()
                self.TmrObj.setInterval(2000)
                self.TmrObj.timeout.connect(self.Streamer)
                
            print('***** Timer Timer On',self.TimrOn)
            if self.TimrOn:
                self.TimrOn = False
                self.TmrObj.stop()
            else:
                self.TimrOn = True
                self.TmrObj.start()
            print('***** Timer Timer On',self.TimrOn)
    
        @pyqtSlot()
        def Streamer(self):
            self.TmrCnt += 1
            self.sigOutTmr.emit(self.TmrCnt)
            print('Thread Timer Count :',self.TmrCnt)
    
        @pyqtSlot()
        def ShutDown(self):
            self.Running = False
            if self.TmrSet:
                self.TmrObj.stop()
                self.TmrObj = QObject()
          # All the Signals within a QThread must be disconnected from within
          # to prevent issues occuring on shutdown
            self.sigOutCur.disconnect()
            self.sigOutTmr.disconnect()
          # Make sure you pass control back to the Event Handler
            QCoreApplication.processEvents()
     
    # A thread handling all the task operations.
    # Starting all tasks from the MainThread would cause blocking
    class TaskRunner(QObject):
        sigNewCntBy = pyqtSignal(int)
        sigTogTime  = pyqtSignal()
        sigShutDwn  = pyqtSignal()
      # Initialization after Signals declared
        def __init__(self, parent, CntBy):
            QObject.__init__(self)
            self.Parent = parent
            self.CountBy = CntBy
    
          # Create the Listener
            self.Tasker = Task(self.CountBy)
          # Assign the Task Signals to Threader Slots
            self.Tasker.sigOutCur.connect(self.ProcessOut)
            self.Tasker.sigOutTmr.connect(self.TimerOut)
          # Assign Threader Signals to the Task Slots
            self.sigNewCntBy.connect(self.Tasker.ReCountr)
            self.sigTogTime.connect(self.Tasker.TogglTimr)
            self.sigShutDwn.connect(self.Tasker.ShutDown)
    
          # Create the Thread
            self.Thred = QThread()
          # Move the Task Object into the Thread
            self.Tasker.moveToThread(self.Thred)
          # Assign the Task Starting Function to the Thread Call
            self.Thred.started.connect(self.Tasker.StartTask)
          # Start the Thread which launches Listener.Connect( )
            self.Thred.start()
    
        @pyqtSlot(int)
        def ProcessOut(self, Number):
            Text = 'Current Count : '+ str(Number)
            self.Parent.txeOutput.append(Text)
    
        @pyqtSlot(int)
        def TimerOut(self, Number):
            Text = 'Timer Count : '+ str(Number)
            self.Parent.txeOutput.append(Text)
    
        def NewCountBy(self, Number):
            self.sigNewCntBy.emit(Number)
    
        def ToggleTimer(self):
            print('Toggling the Threaded Timer')
            self.sigTogTime.emit()
    
        @pyqtSlot(int)
        def ShutDown(self):
            self.sigShutDwn.emit()
            self.Thred.quit()
          # All the Signals outside a QThread must be disconnected from
          # the outside to prevent issues occuring on shutdown
            self.sigNewCntBy.disconnect()
            self.sigShutDwn.disconnect()
          # This waits for the process within the Thread to stop were 
          # upon it finalizes the Quit referenced above
            self.Thred.wait()
    
    
    class CenterPanel(QWidget):
        def __init__(self, parent):
            QWidget.__init__(self)
            self.Parent = parent
    
            print('GUI Lives Within :', QThread.currentThread())
    
            self.TimrStrtd = False
            self.TaskStrtd = False
            self.CountTime = QObject()
            self.CountTask = QObject()
    
            self.btnStart = QPushButton('Tasker')
            self.btnStart.clicked.connect(self.DoTask)
    
            HBox1 = QHBoxLayout()
            HBox1.addWidget(self.btnStart)
            HBox1.addStretch(1)
    
            self.btnTimer = QPushButton('TimerOn')
            self.btnTimer.clicked.connect(self.Timer)
            self.TimerOn = False
    
            HBox2 = QHBoxLayout()
            HBox2.addWidget(self.btnTimer)
            HBox2.addStretch(1)
    
          # Note if you use the X to close the window you 
          # need to capture that event and direct it to do
          # self.ShutDown as well this is just a MUC
            self.btnQuit = QPushButton('Quit')
            self.btnQuit.clicked.connect(self.ShutDown)
    
            HBox3 = QHBoxLayout()
            HBox3.addWidget(self.btnQuit)
            HBox3.addStretch(1)
    
            self.cbxNums = QComboBox()
            self.cbxNums.addItems(['0', '1', '2', '3', '4', '5'])
            self.cbxNums.setCurrentIndex(1)
            self.cbxNums.setMaximumWidth(100)
    
            self.txeOutput = QTextEdit()
            self.txeOutput.setMinimumHeight(50)
    
            self.scrlOutVew = QScrollArea()
            self.scrlOutVew.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
            self.scrlOutVew.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self.scrlOutVew.setWidgetResizable(True)
            self.scrlOutVew.setWidget(self.txeOutput)
    
            VBox = QVBoxLayout()
            VBox.addLayout(HBox3)
            VBox.addLayout(HBox2)
            VBox.addLayout(HBox1)
            VBox.addWidget(self.cbxNums)
            VBox.addWidget(self.scrlOutVew)
    
            self.setLayout(VBox)
    
        @pyqtSlot()
        def DoTask(self):
            Num = int(self.cbxNums.currentText())
            
            if self.TaskStrtd:
                self.CountTask.NewCountBy(Num)
            else:
                self.TaskStrtd = True
                self.CountTask = TaskRunner(self, Num)
    
        @pyqtSlot()
        def Timer(self):
            print('Timer Clicked :',self.TimerOn)
            if not self.TimrStrtd:
                self.TimrStrtd = True
                self.CountTime = TaskRunner(self, 0)
    
            if self.TimerOn:
                self.btnTimer.setText('TimerOn')
                self.TimerOn = False
            else:
                self.btnTimer.setText('TimerOff')
                self.TimerOn = True
            self.CountTime.ToggleTimer()
    
        @pyqtSlot()
        def ShutDown(self):
            self.CountTime.ShutDown()
            self.CountTask.ShutDown()
            self.close()
    
    
    class MainWindow(QMainWindow):
        def __init__(self):
          # One should not use super( ) in Python as it introduces 4 known issues that 
          # then must be handled properly. Further there were still actual bugs within
          # the usage of super( ) when used in Python. So yes while super( ) works fine 
          # within C++ it does not work as seamlessly within Python due to the major 
          # differences between these two languages. Next the reason it was created was 
          # to handle a rather rare issue and unless you are doing some complicated 
          # inheritance you will most likely never run into this extremely rare issue
          # However the 4 major issues that get included by using super( ) are much more
          # likely to occur than that rare issue its meant for to solve. Of course using
          # the basic explicit method, as follows, does not cause these issues and is as
          # just as simple as using `super( )` further you do not actually gain anything
          # useful by using `super( )` in Python that could not be done in a much safer
          # manner.
          # super().__init__()
            QMainWindow.__init__(self)
    
          # It is good practice to keep you QMainWindow to the bare minimums of what it
          # actually needs to handle, these being the Main Window Title & Position, the
          # Central Widget, Menu-Tool Bar, Status Bar, and any Dockable Windows used.
            self.setWindowTitle('Scroll Area Demonstration')
            WinLeft = 600; WinTop = 100; WinWidth = 1000; WinHigh = 900
            self.setGeometry(WinLeft, WinTop, WinWidth, WinHigh)
    
            self.CenterPane = CenterPanel(self)
            self.setCentralWidget(self.CenterPane)
    
            self.StatBar = self.statusBar()
            self.SetStatMsg()
    
        def SetStatMsg(self, StatusMsg=''):
            if len(StatusMsg) < 1:
                StatusMsg = 'Ready'
    
            self.StatBar.showMessage(StatusMsg)
    
     
    if __name__ == '__main__':
      # If you are not using Command Line objects then you do not need sys.argv
      # However if you are using Command Line objects you should look into 
      # argparser library as it handles them much cleaner and more intuitively
      # app = QApplication(sys.argv)
        MainEventHandler = QApplication([])
    
        MainApplication = MainWindow()
        MainApplication.show()
        
      # This is the old PyQt4 method of calling this
      # sys.exit(app.exec_())
      # This is the current PyQt5 method of calling this
        MainEventHandler.exec()
    


  • @eyllanesc I attatched my code here


  • Banned

    This message was delayed 600 seconds due to Troll activity on this forum

    @eyllanesc he did -- its a Link where he says The code can be found here.



  • If a moderator reads me: Is it within the code that a user calls a member of the community as a troll? I think not since it is not a sign of respect. One may agree or disagree with the site's policies or the attitudes of others but that does not give anyone the right to insult


  • Banned

    This message was delayed 600 seconds due to Troll activity on this forum

    @eyllanesc if you are referring to my post you are as usual sadly mistaken -- the first line has nothing to do with you directly unless you are involved in that issue -- so are you saying with your comment you are admitting to doing such? If not then it is what it was -- its a disclaimer letting folks know that the message got delayed (like this one did as well) by 600 seconds due to Troll issues on this forum which is a fact and nothing more nor nothing less. If you cannot understand why, well it is fairly simple and this situation kind of demonstrates it -- my post that had actually occurred before Caelum's post ended up landing after his post due to that delay.

    As to the actual comment it was what it was -- the OP had shared a MRE. So maybe you should be more concerned about your attention to detail or lack there of rather than my disclaimers that are not directed at anyone other than those who have been down-voting as a means of trolling or showing their displeasure or for any other reason than what it was meant for -- as the abuse of it is a form of Trolling. Personally using it at all to me is a form of Trolling because really the forum is about trying to help others and down voting anything rarely helps anyone and is basically done by callous individuals who could care less about helping others as opposed to bolstering their own frail egos.



  • A do agree this discussion was just a misunderstanding and there is no need to get it heated. I'm locking the topic to avoid even more aggravation. If OP is still looking for help, feel free to open a new post and link to here.


Log in to reply