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()
andblockSignals(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
-
All right well several things -- 1st when using Qt you should
only
useQThread
and you should definitely not mixQThread
with Pythonsthreading
as using Pythonsthreading
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
-
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
-
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.