Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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:

    1. click the button
    2. click one more time while function is running
    3. 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)
    4. 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.


  • Moderators

    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#details

    or my GitHub-Example project:
    https://github.com/DeiVadder/QtThreadExample

    for examples of all Qt-ways to do threading/parallelization.



  • @J-Hilk Thanks for the reply, so you're suggesting the use of threads to solve the main problem (unwanted LMB clicks)?


  • Moderators

    @rebus_x If you have a long running function - that doesn't access UI elements!!! - that is blocking your GUI, than yes, using threads is usually the way to go



  • @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 use QtConcurrent::run(). In combination with QFutureWatcher() 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 and processEvents.
    [ADDED]
    To make the button keep disabled when event is called right after the function finishes, I think we should call setEnabled(true) asynchronously. Something like

    button->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

Log in to reply