Qt blockSignals不会阻止第一次额外点击

Qt blockSignals is not blocking first extra click

本文关键字:第一次 blockSignals Qt      更新时间:2023-10-16

我正试图通过在连接到按钮的插槽函数中使用QObject的blockSignals方法,在QPushButton上进行额外的点击。然后,我启动一个排队的连接信号,该连接信号连接到一个插槽,该插槽可以解锁按钮的信号。

我的想法是,像数据库操作这样的阻塞操作,由下面代码中的睡眠调用表示,可能会导致用户额外单击按钮。我解决这个问题的方法是在第一次点击后屏蔽按钮的信号,这样在屏蔽操作期间积累的额外点击就不会起任何作用,自发的事件队列就会完成,然后发布的事件队列会处理排队的信号,从而解锁按钮并使应用程序返回正常状态。

我的问题是,第一次点击会处理,但第一次额外点击(应该被阻止)会意外处理。随后的额外点击不会起任何作用。

这是代码:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = 0);
    ~MainWindow();
private:
    QPushButton *btn;
signals:
    void delayed_unblock();
private slots:
    void doStuff();
    void unblock();
};
#endif // MAINWINDOW_H

其余代码:

#include "mainwindow.h"
#include <QDebug>
#include <QTest>
int num = 0;
void MainWindow::doStuff() {
    qDebug() << btn->signalsBlocked();
    qDebug() << btn;
    qDebug() << sender();
    qDebug() << num++;
    btn->blockSignals(true);
    QTest::qSleep(5000);
    emit(delayed_unblock());
}
void MainWindow::unblock() {
    btn->blockSignals(false);
}
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent),
      btn(new QPushButton("foo"))
{
    connect(btn, &QPushButton::clicked, this, &MainWindow::doStuff);
    connect(this, &MainWindow::delayed_unblock,
            this, &MainWindow::unblock,
            Qt::QueuedConnection);
    setCentralWidget(btn);
}

如果我快速点击按钮4次,调试控制台会这样做:

false
QPushButton(0x3e8840)
QPushButton(0x3e8840)
0
false
QPushButton(0x3e8840)
QPushButton(0x3e8840)
1

我原以为输出只是

false
QPushButton(0x3e8840)
QPushButton(0x3e8840)
0

因为,据我所知,第一次点击会导致同步插槽调用,从而阻止任何后续按钮信号的发生。在我的案例中,随后的信号源自自发事件队列中的鼠标单击事件。看起来,所有额外的点击都应该被阻止,但第一次额外的点击仍然可以通过。

如果有帮助的话,点击一次,等待3秒钟,然后快速点击3次,会得到与上述相同的结果。

我的编译器是MSVC 2015。

深入研究Qt的调度机制可以显示按哪个顺序发生的事情。由此可见,为什么会出现这种奇怪的行为。

从回溯中,我深入了解了Qt用来调度事件的glib 2.0的g_main_context_dispatch。

在该功能中,事件被分为不同的组(队列)。例如,首先是所有发布的事件,然后是所有X11/Windows事件,等等(如果有的话)。

请注意,一次鼠标点击,包括一个新闻发布事件,通常会导致两个连续的队列,因为它们处理得太快了。

意味着"按下"进入队列,包含该SINGLE事件的队列或多或少会立即处理,"释放"进入下一个队列,也会立即处理(总是在程序触发的一些发布事件之后)。

只有当处理某些事情(如qSleep())需要更长的时间时,队列中每个组可能包含多个事件。

我设置了三个断点,并在主窗口和按钮上安装了一个事件过滤器。通过这种方式,我们可以看到一切是如何相互作用的。

gdb$ info breakpoints
Num     Type           Disp Enb Address    What
2       breakpoint     keep y   0xb6d41db2 <g_main_context_dispatch+578>
        breakpoint already hit 549 times
        silent
        p "dispatch"
        continue
4       breakpoint     keep y   0xb6d41bdd <g_main_context_dispatch+109>
        breakpoint already hit 546 times
        silent
        p $ebp
        continue
5       breakpoint     keep y   0xb6d41bc9 <g_main_context_dispatch+89>
        breakpoint already hit 551 times
        silent
        p "leaving"
        continue

请注意,我在所有断点命令中都加了"continue"。因此,调试在任何时候都没有阻止该应用程序。

连续点击四次(在qSleep()中点击三次)的结果如下:

// Dispatch function entered, one queue with events available
$1528 = (void *) 0x1
// dispatching results in the pressEvent received by the button
$1529 = "dispatch"
"QPushButton(0x809d128) press" 
// dispatch function left, the spontaneous event queue
// contained only the mouse press
$1530 = "leaving"
// again entering with events in 2 queues, no idea what for
$1531 = (void *) 0x2
// dispatching of both doesn't result in press or release events
$1532 = "dispatch"

$1533 = "dispatch"

$1534 = "leaving"
// Huh, another leaving, obviously no events in any queue
$1535 = "leaving"
// Once more dispatching with nothing of interest for us
$1536 = (void *) 0x1

$1537 = "dispatch"

$1538 = "leaving"
// here comes the queue containing the release event
// of the first click
$1539 = (void *) 0x1
// the dispatch results in the release event and the button
// triggers the doStuff() function.
$1540 = "dispatch"
"QPushButton(0x809d128) release" 
false 
0
// -----
// Now the qSleep() runs for 5 secs. I clicked 3 times.
// There is no way Qt can process the individual presses
//and releases. The window system buffers them until Qt has time. 
// -----
// qSleep() finished, the signal for UNBLOCKING is emitted
// and the connected signal is enqueued in the posted events queue.
// -----
// leave the dispatching function
$1541 = "leaving"
// -----
// Now Qt receives the three remaining mouse clicks at once
// and puts ALL of them in a SINGLE spontaneous queue.
// enters the dispatching function, two queues contain events
$1542 = (void *) 0x2
// first queue dispatched, the one with the posted event
// unblocking occurs
$1543 = "dispatch"
"MainWindow(0xbfffe180) queued"
unblock() 
// second queue dispatched,
// the one with the THREE press/release pairs !!!
$1544 = "dispatch"
// first press/release pair triggers button clicked signal
// and that in turn the signal blocking
"QPushButton(0x809d128) press" 
"QPushButton(0x809d128) release" 
false 
1
// ----- 
// now the signals are blocked and qSleep() runs
// qSleep() finished and the signal for UNBLOCKING is emitted
// and the connected signal enqueued in the posted events queue. 
// follwing two press/release pairs don't trigger the
// clicked signal (due to the blocking)
// -----
"QPushButton(0x809d128) press"
"QPushButton(0x809d128) release"
"QPushButton(0x809d128) press"
"QPushButton(0x809d128) release" 
// leaving dispatch function
$1545 = "leaving"
// entering again the dispatch function with two queues
// containing events
$1546 = (void *) 0x2
// the unblocking
$1547 = "dispatch"
"MainWindow(0xbfffe180) queued" 
unblock() 
// and something unknown
$1548 = "dispatch"

$1549 = "leaving"

因此,为什么发布的解锁会干扰点击就变得很奇怪了没有机会阻止所有点击,因为qt在其他事件之前处理发布的事件(解锁)只有缓冲点击的整体处理"似乎"有效。

如果阻塞信号与耗时的处理结合使用,请记住这一点。

这也解释了为什么我使用QMetaObject::invokeMethod调用SIGNAL的"破解"(见下文)有效它会导致重定向,并且需要两个已发布的事件第一个用于SIGNAL(用emit()立即调用),第二个用于SLOT。只有到那时才会解除阻止。到那时,在按钮仍然静音的情况下,额外的点击已经发出:

1. click // dispatched, blocking
qSleep() // meanwhile clicking 3 times
         // followed by enqueuing the SIGNAL
         // followed by enqueuing the 3 clicks in a single queue
unblocking SIGNAL // dispatched, unblocks not yet
2.,3., and 4. click // dispatched, but button still blocked
unblocking SLOT // dispatched, finally unblocks

在我下面的"解决方案"中,使用非阻塞本地事件循环而不是阻塞qSleep(),三次单击将立即处理(而不是在取消阻塞排队之后),并且不会发出任何信号


解决方案同时保持qSleep():

我使用QMetaObject::invokeMethod()调用SIGNAL:解决了这个问题

QMetaObject::invokeMethod(this, "delayed_unblock", Qt::QueuedConnection);

而不是emit(delayed_unblock());


解决方案使用本地事件循环:

void MainWindow::doStuff()
{
qDebug() << btn->signalsBlocked();
qDebug() << num++;
btn->blockSignals(true);
QTimer t;
t.setSingleShot(true);
t.setInterval(5000);
QEventLoop loop;
connect(&t, SIGNAL(timeout()), &loop, SLOT(quit()));
t.start();
loop.exec(); // lets event processing happen nothing blocked (no mopuseclicks stuck in the windows system !?)
//QMetaObject::invokeMethod(this, "delayed_unblock", Qt::QueuedConnection);
emit(delayed_unblock());
}

解决方案在qSleep()之后立即使用processEvents

QApplication::processEvents();似乎立即接收和调度windows系统事件。这也解决了问题。


由于历史原因留下了以下几行;)

因为Qt 5.6的文档告诉我们"被阻塞时发出的信号不会被缓冲。"。。由于qSleep()阻止了应用程序,因此根本不会发出任何内容。彻底地因此,在qSleep()完成之前,Qt根本无法控制点击的鼠标按钮(在Windows或X11中仍然存在)。应该是由于windows系统缓冲了点击。第一次点击是在计时器结束后处理的,包括取消阻止。对于剩余的点击,到那时信号再次被阻断。(@thuga很好地解释了这一点)。

据我所知,以下是发生的情况:

  1. 你点击按钮
  2. 插槽被调用
  3. 你挡住了按钮的信号
  4. 使用QTest::qSleep阻止事件循环
  5. 您再次单击(由于您阻止了该事件,该操作尚未处理回路)
  6. QTest::qSleep退出
  7. 您调用排队的调用来取消阻止按钮信号
  8. 应用程序返回到事件循环
  9. 处理排队呼叫MainWindow::unblock()
  10. 然后它会处理您在事件发生时进行的额外点击循环被阻止

但是,如果在事件循环被阻止时单击了多次,它将在第二次调用MainWindow::unblock()之前处理这些事件。这就是为什么即使你连续点击按钮4次,你的老虎机也只会被调用两次。