关于c ++:Qt blockSignals不会阻止第一次额外的点击

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先于其他事件处理发布的事件(取消阻止)。仅缓冲单击的整体处理"似乎"起作用。

如果结合使用阻塞信号和耗时的处理,则最好记住这一点。

这也解释了为什么我使用QMetaObject::invokeMethod调用SIGNAL的" hack"(见下文)的原因。它导致重定向,并且需要两个发布的事件。首先是SIGNAL(否则立即用emit()调用),其次是SLOT。只有这样,解锁才会发生。到那时,在按钮仍处于静音状态时,已经发出了额外的点击:

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()的解决方案:

我通过使用QMetaObject::invokeMethod()调用SIGNAL解决了该问题:

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

代替emit(delayed_unblock());

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

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的解决方案

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

由于历史原因离开了后面的台词; )

因为Qt 5.6的文档告诉我们"被阻塞时发出的信号不会被缓冲"。考虑一下..因为qSleep()阻止了该应用程序,所以什么也没有发出。完全。因此,应该是在QSleep()完成之前,Qt根本无法完全抓住所单击的鼠标按钮(仍然停留在Windows或X11中)。这应该归功于Windows系统对点击的缓冲。计时器结束后,所有其他事情(包括解锁)都会处理第一次点击。对于其余的喀哒声,信号将再次被阻塞。 (@thuga很好地解释了这一点)。


据我所知,这是发生了什么:

  • 您单击按钮
  • 插槽被称为
  • 您阻止按钮的信号
  • 您使用QTest::qSleep阻止事件循环
  • 您再次单击(阻止活动后,此操作尚未处理
    环)
  • QTest::qSleep退出
  • 您调用排队呼叫以解除阻塞按钮信号
  • 应用程序返回事件循环
  • 处理您对MainWindow::unblock()的排队呼叫
  • 然后它会处理您之前在活动期间做出的额外点击
    循环被阻止
  • 但是,如果在事件循环被阻止时多次单击,它将在第二次调用MainWindow::unblock()之前处理这些事件。 这就是为什么即使您连续四次单击按钮,插槽也只能被调用两次的原因。