Qt blockSignals is not blocking first extra click
我试图通过在连接到该按钮的插槽函数中使用QObject的blockSignals方法,对QPushButton进行额外的单击,从而无济于事。然后,我发出一个排队的连接信号,该信号连接到一个插槽,该插槽解除了按钮信号的阻塞。
我的想法是,阻塞操作(如数据库操作)(由下面的代码中的sleep调用表示)可能会导致用户额外单击按钮。我解决此问题的方法是在第一次单击后阻止按钮的信号,以使在阻止操作过程中累积的额外单击不起作用,自发事件队列将完成,然后发布的事件队列将处理排队的信号,将取消阻止按钮并使应用程序返回其正常状态。
我的问题是第一次单击处理,但是应该阻止的第一次额外单击却意外处理。随后的额外点击没有任何作用。
这是代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | #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 |
...以及其余代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | #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次,调试控制台将执行以下操作:
1 2 3 4 5 6 7 8 | false QPushButton(0x3e8840) QPushButton(0x3e8840) 0 false QPushButton(0x3e8840) QPushButton(0x3e8840) 1 |
我期望输出只是
1 2 3 4 | false QPushButton(0x3e8840) QPushButton(0x3e8840) 0 |
因为据我所知,第一次单击会导致同步插槽调用,该调用可以阻止随后发生的任何按钮信号。在我的情况下,随后的信号源自自发事件队列中的鼠标单击事件。看来,所有额外的点击都应该被阻止,但那第一次额外的点击仍在进行中。
如果有帮助,请单击一次,等待3秒钟,然后快速单击3次,将得到与上面相同的结果。
我的编译器是MSVC 2015。
深入研究Qt的分派机制将显示按什么顺序发生的事情。由此可见,为什么会发生这种奇怪的行为。
从回溯中,我挖掘了Qt用于调度事件的glib 2.0的g_main_context_dispatch。
在此功能内,事件分为两个单独的组(队列)。例如。首先是所有发布的事件,然后是所有X11 / Windows事件,等等。
请注意,一次鼠标单击(包括按下和释放事件)通常会导致两个连续的队列,因为它们的处理速度如此之快。
表示"按"进入队列,该队列包含立即处理或几乎立即处理的单个SINGLE事件,"释放"进入下一个队列并也立即处理(总是在程序触发某些已发布事件之后)。
仅当处理某些内容(例如qSleep())花费的时间更长时,每个组中的队列才可能包含多个事件。
我设置了三个断点,并在主窗口和按钮上安装了一个事件过滤器。这样,就有可能看到所有事物如何相互作用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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()中的三个)如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | // 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先于其他事件处理发布的事件(取消阻止)。仅缓冲单击的整体处理"似乎"起作用。
如果结合使用阻塞信号和耗时的处理,则最好记住这一点。
这也解释了为什么我使用
1 2 3 4 5 6 7 8 9 10 11 | 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()的解决方案:
我通过使用
1 | QMetaObject::invokeMethod(this,"delayed_unblock", Qt::QueuedConnection); |
代替
使用本地事件循环的解决方案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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的解决方案
由于历史原因离开了后面的台词; )
因为Qt 5.6的文档告诉我们"被阻塞时发出的信号不会被缓冲"。考虑一下..因为qSleep()阻止了该应用程序,所以什么也没有发出。完全。因此,应该是在QSleep()完成之前,Qt根本无法完全抓住所单击的鼠标按钮(仍然停留在Windows或X11中)。这应该归功于Windows系统对点击的缓冲。计时器结束后,所有其他事情(包括解锁)都会处理第一次点击。对于其余的喀哒声,信号将再次被阻塞。 (@thuga很好地解释了这一点)。
据我所知,这是发生了什么:
环)
循环被阻止
但是,如果在事件循环被阻止时多次单击,它将在第二次调用