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

标签 c++ qt events queue

我试图通过在连接到按钮的槽函数中使用 QObject 的 blockSignals 方法来对 QPushButton 进行额外的单击而不执行任何操作。然后,我触发一个排队的连接信号,该信号连接到一个插槽,该插槽可以解锁按钮的信号。

我的想法是,像数据库操作这样的阻塞操作(由下面代码中的 sleep 调用表示)可能会导致用户额外单击按钮。我解决此问题的方法是在第一次单击后阻止按钮的信号,以便在阻止操作期间累积的额外点击不会执行任何操作,自发事件队列将完成,然后发布的事件队列将处理排队的信号将解锁按钮并将应用程序返回到正常状态。

我的问题是第一次点击会被处理,但是第一次额外的点击(应该被阻止)却意外地进行了处理。随后的额外点击不会执行任何操作。

这是代码:

#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 事件等(如果有)。

请注意,由按下和释放事件组成的单次鼠标单击通常会导致两个连续的队列,因为它们的处理速度非常快。

表示“按下”进入队列,该队列包含此单个事件或多或少立即处理,“释放”进入下一个队列并且也立即处理(总是在程序触发的一些发布事件之后或所以)。

只有当处理某些事情(例如 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

请注意,我在所有断点命令中都添加了“继续”。因此调试在任何时候都不会阻止应用程序。

连续四次点击(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()调用信号解决了这个问题:

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 解释得很好)。

关于c++ - Qt blockSignals 不会阻止第一次额外点击,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37288084/

相关文章:

qt - 使用 Qt 的 Cosmos DB

c++ - 在 QTextEdit 中对齐文本?

javascript - 防止对象函数内的事件覆盖 "this"

image - 我可以在图像标签内使用 onclick() 事件吗?

javascript - 使用键盘快捷键检查的 radio 输入不会触发事件更改

c++ - 为什么 C++ 中没有隐式按位比较?

java - 对于初学者来说,如何在 Emacs 中首次启用自动完成功能?

c++ - 从结构 vector 中获取特定成员的 vector

C++ 错误此声明没有存储类或类型说明符

javascript - JS 和 QTDatetime 中的 Unix 时间戳转换之间的区别?