QApplication::processEvents() not handling all mouse click events, have to call it twice in a row
-
Hi. I have a QPushButton that triggers a long-running function. My problem is if the button is clicked during that function running, additional mouse-click events are added to event queue.
So I use an event filter to get rid of those, and that is done by calling processEvents() in function itself. But for some reason, if processEvents() is called once, it doesn't get rid of all the click events, and I have to call it once more. That seems to fix the problem, but I'd like to know why it's happening or if I'm doing something wrong; I read about calling processEvents() twice here, and it's briefly mentioned about some bug being the reason of this, which I sadly haven't found any information about.
I did a small example to illustrate what I'm talking about.
widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QPushButton> #include <QWidget> class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget() override; QPushButton *button; bool working = false; void dumbFunction(); virtual bool eventFilter(QObject *watched, QEvent *event) override; }; #endif // WIDGET_H
widget.cpp
#include "widget.h" #include <QApplication> #include <QDebug> #include <QThread> Widget::Widget(QWidget *parent) : QWidget(parent) { button = new QPushButton("pressme", this); //connect(button, &QPushButton::clicked, this, &Widget::dumbFunction); //<- happens less if call function through signal button->installEventFilter(this); } Widget::~Widget() { } void Widget::dumbFunction() { static int count = 0; working = true; button->setDisabled(true); button->repaint(); qInfo() << "Me, useless" << ++count; QThread::sleep(2); QApplication::instance()->processEvents(); //QApplication::instance()->processEvents(); //<- uncomment for proper behavior button->setEnabled(true); working = false; } bool Widget::eventFilter([[maybe_unused]] QObject *watched, QEvent *event) { if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonDblClick) { if (working) qInfo() << "...filtered!"; else dumbFunction(); return true; } return false; }
main.h
#include "widget.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); return a.exec(); }
To reproduce:
- click the button
- click one more time while function is running
- in the output, you'll see that the additional click was not filtered but triggered the function again (only 1st additional click is not filtered)
- uncomment second processEvents() call, now it works as it should
I use Qt 5.13.1 64-bit, both the example and my main application have only main thread.
-
You're using processEvents() in the first place, there is always a better way to do what one (actually) wants. And it has almost always unforeseeable consequences, like reentering a function while it's executed!
Don't use it.
See the details section of QThread for two examples
https://doc.qt.io/qt-5/qthread.html#detailsor my GitHub-Example project:
https://github.com/DeiVadder/QtThreadExamplefor examples of all Qt-ways to do threading/parallelization.
-
@rebus_x said in QApplication::processEvents() not handling all mouse click events, have to call it twice in a row:
Thanks for the reply, so you're suggesting the use of threads to solve the main problem (unwanted LMB clicks)?
As @J-Hilk already told you, locking the main thread is never the best way to do.
If you only want to wait a long processing function to finish, and this function do not interact with UI elements, you can considere to useQtConcurrent::run()
. In combination withQFutureWatcher()
you can also be informed when concurrent function is finished and got returned value.Take a look at the documentation ==> https://doc.qt.io/qt-5/qtconcurrentrun.html
-
@rebus_x said in QApplication::processEvents() not handling all mouse click events, have to call it twice in a row:
(unwanted LMB clicks)
If you don't mind the function blocking the ui, I usually simply do
button->setEnabled(false); //call the long-running function button->setEnabled(true);
-
@Bonnie Yes, but this unfortunately doesn't prevent click events from happening, which is mentioned in QAbstractButton documentation:
Note: As opposed to other widgets, buttons derived from QAbstractButton accept mouse and context menu events when disabled.
In my example I do the same thing with disabling and enabling
-
@rebus_x
Yes, I know. But disabling the button will make it not pressable, which is also mentioned in the doc:isEnabled() indicates whether the button can be pressed by the user.
Actually if you look into the source code of QAbstractButton, you can see it will filter out the mouse press / release / dblclick events in its own
event
function when disabled.bool QAbstractButton::event(QEvent *e) { // as opposed to other widgets, disabled buttons accept mouse // events. This avoids surprising click-through scenarios if (!isEnabled()) { switch(e->type()) { case QEvent::TabletPress: case QEvent::TabletRelease: case QEvent::TabletMove: case QEvent::MouseButtonPress: case QEvent::MouseButtonRelease: case QEvent::MouseButtonDblClick: case QEvent::MouseMove: case QEvent::HoverMove: case QEvent::HoverEnter: case QEvent::HoverLeave: case QEvent::ContextMenu: #if QT_CONFIG(wheelevent) case QEvent::Wheel: #endif return true; default: break; } } ... }
So I don't really understand why do you add those
eventFilter
andprocessEvents
.
[ADDED]
To make the button keep disabled whenevent
is called right after the function finishes, I think we should callsetEnabled(true)
asynchronously. Something likebutton->setEnabled(false); //call the long-running function QMetaObject::invokeMethod(button, "setEnabled", Qt::QueuedConnection, Q_ARG(bool, true));
-
@Bonnie Concerning disabled buttons behavior - you are correct, turns out that in the docs quote that I posted "accept" actually means "accept()".
I tinkered a little more with the problem and looks like I got the answer.
- As the main thread is blocked, a button can't work on events, so they go somewhere in OS event queue
- first call of processEvents() right after long action(s) won't see any events. But it seems that it triggers something that grabs events from OS and moves them to thread event queue
- because of that, future calls of processEvents() deal with those