Issue
In my program I have a button that is connected to a function that takes about 15 seconds to run. I want to ignore all user inputs during this run time. When btn
is pressed the following function is called:
def btn_call(self) :
self.btn.setEnable(False) # disables the button and shows it greyed out
fn() # some function that takes ~15 seconds to run
self.btn.setEnable(True) # re-enables the button
The hope was to keep the program from responding to btn
presses while fn()
is running. Currently, if btn
is pressed while fn()
is running, fn()
will run for each time btn
was pressed.
Is there a way to clear all user inputs that occurred while fn()
runs?
Edit:
Added a MWE. If you click Run Function.
the function will begin. If you click Run Function.
while it is still running, the function will run a second time. This is the behaviour that I would like to stop.
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
from time import sleep
def fn(num) :
for i in range(num) :
sleep(0.5)
yield i
class MainWindow(QWidget) :
def __init__(self) :
super().__init__()
self.layout = QVBoxLayout(self)
self.btn_fn = QPushButton("Run function.")
self.btn_fn.clicked.connect(self.run_fn)
self.layout.addWidget(self.btn_fn)
self.prog_bar = QProgressBar()
self.layout.addWidget(self.prog_bar)
self.show()
def run_fn(self) :
self.btn_fn.setEnabled(False)
num = 20
self.prog_bar.setValue( 0 )
for i in fn(num) :
self.prog_bar.setValue( 100*(i+1)/num )
self.btn_fn.setEnabled(True)
if __name__ == '__main__' :
app = QApplication(sys.argv)
window = MainWindow()
sys.exit( app.exec_() )
Solution
When a blocking function is run in the main Qt thread, the result is that all events are queued in that thread. All events, including repainting and mouse events.
The QApplication will still receive incoming events from the OS though, and it will add them to its event queue until that blocking function return.
This results in the following important aspects:
- all time-demanding operation on the Qt side will be blocked until the control is returned to the Qt event loop;
- no widget repainting will be correctly completed if animations are involved (so the button will still probably look as enabled, even if it isn't, due to the previous point);
- all keyboard/mouse events that the widget could possibly receive will be queued and actually processed only as soon as the control has returned to the main Qt event loop;
What you could theoretically do is to block all signals from the button as soon as the time taking function is going to start, then use a singleshot QTimer (which ensures that its timeout
call is processed as soon as all the Qt queue is cleared, including timed events) to unblock signal emission.
This is clear in the following example:
from PyQt5 import QtCore, QtWidgets
from time import sleep
def veryLongFunction():
sleep(5)
def restoreButton():
label.setText('Idle')
button.blockSignals(False)
button.setEnabled(True)
def start():
label.setText('Working...')
button.setEnabled(False)
button.blockSignals(True)
QtWidgets.QApplication.processEvents()
veryLongFunction()
QtWidgets.QApplication.processEvents()
QtCore.QTimer.singleShot(0, restoreButton)
import sys
app = QtWidgets.QApplication(sys.argv)
widget = QtWidgets.QWidget()
button = QtWidgets.QPushButton('Start long function')
label = QtWidgets.QLabel('Idle', alignment=QtCore.Qt.AlignCenter)
layout = QtWidgets.QVBoxLayout(widget)
layout.addWidget(button)
layout.addWidget(label)
geo = QtCore.QRect(0, 0, 160, 80)
geo.moveCenter(app.primaryScreen().availableGeometry().center())
widget.setGeometry(geo)
widget.show()
button.clicked.connect(start)
sys.exit(app.exec_())
With the above example, any mouse click sent to the button will be ignored until the button is enabled again, which technically solve your issue.
But, no, this is not a good approach: it's a very bad one.
Not only you'll have update issues on platforms that do not support composition or double buffering of the previously painted device (moving the window while processing will show graphical artifacts), but, most importantly, it's not the correct approach.
As long as the "time demanding" process is only going to be done individually (meaning that no concurrent processing is required), QThread is the best option:
# ...
class VeryLongFunctionThread(QtCore.QThread):
def run(self):
sleep(5)
def start():
label.setText('Working...')
button.setEnabled(False)
veryLongFunction.start()
def end():
label.setText('Idle')
button.setEnabled(True)
# ...
button.clicked.connect(start)
veryLongFunction = VeryLongFunctionThread()
veryLongFunction.finished.connect(end)
# ...
In the case that the processing needs to be run more than once concurrently, the alternative is to use QRunnable, but remember that it does not inherit from QObject (so it has no support for signals), and you need to find a way to notify the main thread about its status.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.