PyQt 清除未完成的用户输入

PyQt clear outstanding user input

在我的程序中,我有一个按钮连接到一个函数,该函数需要大约 15 秒才能到达 运行。我想在此 运行 时间内忽略所有用户输入。当按下 btn 时,将调用以下函数:

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

希望在 fn() 为 运行ning 时阻止程序响应 btn 按键。目前,如果在 fn() 处于 运行ning 时按下 btn,则每次按下 btn 时,fn() 将 运行。

有没有办法清除在 fn() 运行 秒时发生的所有用户输入?

编辑: 添加了一个 MWE。如果您单击 Run Function.,该功能将开始。如果在 运行ning 时单击 Run Function.,该函数将再次 运行。这是我想停止的行为。

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_() )

当一个blocking函数在主Qt线程中运行时,结果是所有事件都queued在那个线程。 所有 事件,包括重绘和鼠标事件。

尽管如此,QApplication 仍将接收来自 OS 的传入事件,并将它们添加到其事件队列中,直到阻塞函数 return.

这导致以下重要方面:

  • Qt 端的所有time-demanding 操作将被阻塞,直到控件returned 到Qt 事件循环;
  • 如果涉及动画,则不会正确完成小部件重绘(因此按钮仍可能看起来已启用,即使它未启用,由于前一点);
  • 控件可能接收到的所有 keyboard/mouse 事件将被排队并仅在控件 returned 到主 Qt 事件循环后才实际处理;

可以理论上做的是在计时功能开始时立即阻止来自按钮的所有信号,然后使用单发 QTimer(确保它的 timeout 调用在 所有 Qt 队列被清除后立即处理,包括定时事件)以解锁信号发射。

这在下面的例子中很清楚:

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_())

对于上面的例子,发送到按钮的任何鼠标点击都将被忽略,直到再次启用该按钮,这从技术上解决了您的问题。

但是,不,这不是一个好方法:这是一个非常糟糕的方法。
不仅在不支持以前绘制的设备的组合或双缓冲的平台上你会遇到更新问题(在处理时移动 window 将显示图形工件),而且最重要的是,它不是 正确的方法。

只要“时间要求高”的过程只是单独完成(意味着不需要并发处理),QThread 是最佳选择:

# ...
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)

# ...

如果需要同时处理运行多次,替代方案是使用QRunnable,但请记住它不是继承自QObject (所以它不支持信号),你需要找到一种方法来通知主线程它的状态。