QSpinBox inside a QScrollArea: How to prevent Spin Box from stealing focus when scrolling?
Asked Answered
M

7

25

I have a control with several QSpinBox objects inside a QScrollArea. All works fine when scrolling in the scroll area unless the mouse happens to be over one of the QSpinBoxes. Then the QSpinBox steals focus and the wheel events manipulate the spin box value rather than scrolling the scroll area.

I don't want to completely disable using the mouse wheel to manipulate the QSpinBox, but I only want it to happen if the user explicitly clicks or tabs into the QSpinBox. Is there a way to prevent the QSpinBox from stealing the focus from the QScrollArea?

As said in a comment to an answer below, setting Qt::StrongFocus does prevent the focus rect from appearing on the control, however it still steals the mouse wheel and adjusts the value in the spin box and stops the QScrollArea from scrolling. Same with Qt::ClickFocus.

Meister answered 28/4, 2011 at 16:19 Comment(0)
T
17

Try removing Qt::WheelFocus from the spinbox' QWidget::focusPolicy:

spin->setFocusPolicy( Qt::StrongFocus );

In addition, you need to prevent the wheel event from reaching the spinboxes. You can do that with an event filter:

explicit Widget( QWidget * parent=0 )
    : QWidget( parent )
{
    // setup ...
    Q_FOREACH( QSpinBox * sp, findChildren<QSpinBox*>() ) {
        sp->installEventFilter( this );
        sp->setFocusPolicy( Qt::StrongFocus );
    }

}

/* reimp */ bool eventFilter( QObject * o, QEvent * e ) {
    if ( e->type() == QEvent::Wheel &&
         qobject_cast<QAbstractSpinBox*>( o ) )
    {
        e->ignore();
        return true;
    }
    return QWidget::eventFilter( o, e );
}

edit from Grant Limberg for completeness as this got me 90% of the way there:

In addition to what mmutz said above, I needed to do a few other things. I had to create a subclass of QSpinBox and implement focusInEvent(QFocusEvent*) and focusOutEvent(QFocusEvent*). Basically, on a focusInEvent, I change the Focus Policy to Qt::WheelFocus and on the focusOutEvent I change it back to Qt::StrongFocus.

void MySpinBox::focusInEvent(QFocusEvent*)
{
     setFocusPolicy(Qt::WheelFocus);
}

void MySpinBox::focusOutEvent(QFocusEvent*)
{
     setFocusPolicy(Qt::StrongFocus);
}

Additionally, the eventFilter method implementation in the event filter class changes its behavior based on the current focus policy of the spinbox subclass:

bool eventFilter(QObject *o, QEvent *e)
{
    if(e->type() == QEvent::Wheel &&
       qobject_cast<QAbstractSpinBox*>(o))
    {
        if(qobject_cast<QAbstractSpinBox*>(o)->focusPolicy() == Qt::WheelFocus)
        {
            e->accept();
            return false;
        }
        else
        {
            e->ignore();
            return true;
        }
    }
    return QWidget::eventFilter(o, e);
}
Tillandsia answered 28/4, 2011 at 16:25 Comment(10)
Setting Qt::StrongFocus does prevent the focus rect from appearing on the control, however it still steals the mouse wheel and adjusts the value in the spin box and stops the QScrollArea from scrolling.Meister
Then you also need to filter the wheel events from reaching QSpinBox. I've extended my answer.Tillandsia
Unfortunately, the event filter also filters out the wheel events I do want on the spin box, ie, when the box is explicitly selected via clicking in the edit area, or tab focusing to the edit area.Meister
@Grant: have you tried just asking the spinbox for hasFocus() in the event filter?Tillandsia
@mmutz: yep. When Qt::StrongFocus is set, a wheel event removes focus from the widget, thus hasFocus() while Qt::StrongFocus is set will always return false on a wheel event.Meister
@Grant: ok. BTW: you don't need to subclass from QSpinBox, you can use the same event filter for checking for focus in/out events, too.Tillandsia
If you subclass QSpinBox, don't forget to add calls to superclass methods QSpinBox::focusOutEvent(event) and QSpinBox::focusInEvent(event) if you don't want strange behaviors.Cynthla
With this answer, I found I also needed to set the containing QScrollArea to have a strong focus policy to prevent it stealing text focus from the spinbox when you scroll the window.Gayelord
-1 The code is not tested. Please see last sample. QObject* defined as obj but referred as o. The same applies also for event variable.Anasarca
@ValentinHeinitz: Maybe you could edit the code instead of downvoting? Nevermind...Tillandsia
L
19

In order to solve this, we need to care about the two following things:

  1. The spin box mustn't gain focus by using the mouse wheel. This can be done by setting the focus policy to Qt::StrongFocus.
  2. The spin box must only accept wheel events if it already has focus. This can be done by reimplementing QWidget::wheelEvent within a QSpinBox subclass.

Complete code for a MySpinBox class which implements this:

class MySpinBox : public QSpinBox {

    Q_OBJECT

public:

    MySpinBox(QWidget *parent = 0) : QSpinBox(parent) {
        setFocusPolicy(Qt::StrongFocus);
    }

protected:

    virtual void wheelEvent(QWheelEvent *event) {
        if (!hasFocus()) {
            event->ignore();
        } else {
            QSpinBox::wheelEvent(event);
        }
    }
};

That's it. Note that if you don't want to create a new QSpinBox subclass, then you can also use event filters instead to solve this.

Labdanum answered 15/10, 2013 at 13:40 Comment(3)
This way works for me instead of the accepted answer. However for the spin to work when the MySpinBox does have focus, you need to also override focusInEvent and focusOutEvent to set focus policy to Qt::WheelFocus when focus in and set back to Qt::StrongFocus when focus out.Gilmore
No, changing the focus policy isn't necessary, the above code works just fine as is. Note, however, that you can only change the spin box value by using the mouse wheel if 1) the spin box has focus and 2) the mouse cursor is placed over the spin box when wheeling.Labdanum
Well. Doing this with PySide turns the SpinBox to never have focus on a wheelEvent. overriding the focusIn/OutEvents did it!Escargot
T
17

Try removing Qt::WheelFocus from the spinbox' QWidget::focusPolicy:

spin->setFocusPolicy( Qt::StrongFocus );

In addition, you need to prevent the wheel event from reaching the spinboxes. You can do that with an event filter:

explicit Widget( QWidget * parent=0 )
    : QWidget( parent )
{
    // setup ...
    Q_FOREACH( QSpinBox * sp, findChildren<QSpinBox*>() ) {
        sp->installEventFilter( this );
        sp->setFocusPolicy( Qt::StrongFocus );
    }

}

/* reimp */ bool eventFilter( QObject * o, QEvent * e ) {
    if ( e->type() == QEvent::Wheel &&
         qobject_cast<QAbstractSpinBox*>( o ) )
    {
        e->ignore();
        return true;
    }
    return QWidget::eventFilter( o, e );
}

edit from Grant Limberg for completeness as this got me 90% of the way there:

In addition to what mmutz said above, I needed to do a few other things. I had to create a subclass of QSpinBox and implement focusInEvent(QFocusEvent*) and focusOutEvent(QFocusEvent*). Basically, on a focusInEvent, I change the Focus Policy to Qt::WheelFocus and on the focusOutEvent I change it back to Qt::StrongFocus.

void MySpinBox::focusInEvent(QFocusEvent*)
{
     setFocusPolicy(Qt::WheelFocus);
}

void MySpinBox::focusOutEvent(QFocusEvent*)
{
     setFocusPolicy(Qt::StrongFocus);
}

Additionally, the eventFilter method implementation in the event filter class changes its behavior based on the current focus policy of the spinbox subclass:

bool eventFilter(QObject *o, QEvent *e)
{
    if(e->type() == QEvent::Wheel &&
       qobject_cast<QAbstractSpinBox*>(o))
    {
        if(qobject_cast<QAbstractSpinBox*>(o)->focusPolicy() == Qt::WheelFocus)
        {
            e->accept();
            return false;
        }
        else
        {
            e->ignore();
            return true;
        }
    }
    return QWidget::eventFilter(o, e);
}
Tillandsia answered 28/4, 2011 at 16:25 Comment(10)
Setting Qt::StrongFocus does prevent the focus rect from appearing on the control, however it still steals the mouse wheel and adjusts the value in the spin box and stops the QScrollArea from scrolling.Meister
Then you also need to filter the wheel events from reaching QSpinBox. I've extended my answer.Tillandsia
Unfortunately, the event filter also filters out the wheel events I do want on the spin box, ie, when the box is explicitly selected via clicking in the edit area, or tab focusing to the edit area.Meister
@Grant: have you tried just asking the spinbox for hasFocus() in the event filter?Tillandsia
@mmutz: yep. When Qt::StrongFocus is set, a wheel event removes focus from the widget, thus hasFocus() while Qt::StrongFocus is set will always return false on a wheel event.Meister
@Grant: ok. BTW: you don't need to subclass from QSpinBox, you can use the same event filter for checking for focus in/out events, too.Tillandsia
If you subclass QSpinBox, don't forget to add calls to superclass methods QSpinBox::focusOutEvent(event) and QSpinBox::focusInEvent(event) if you don't want strange behaviors.Cynthla
With this answer, I found I also needed to set the containing QScrollArea to have a strong focus policy to prevent it stealing text focus from the spinbox when you scroll the window.Gayelord
-1 The code is not tested. Please see last sample. QObject* defined as obj but referred as o. The same applies also for event variable.Anasarca
@ValentinHeinitz: Maybe you could edit the code instead of downvoting? Nevermind...Tillandsia
L
12

My attempt at a solution. Easy to use, no subclassing required.

First, I created a new helper class:

#include <QObject>

class MouseWheelWidgetAdjustmentGuard : public QObject
{
public:
    explicit MouseWheelWidgetAdjustmentGuard(QObject *parent);

protected:
    bool eventFilter(QObject* o, QEvent* e) override;
};

#include <QEvent>
#include <QWidget>

MouseWheelWidgetAdjustmentGuard::MouseWheelWidgetAdjustmentGuard(QObject *parent) : QObject(parent)
{
}

bool MouseWheelWidgetAdjustmentGuard::eventFilter(QObject *o, QEvent *e)
{
    const QWidget* widget = static_cast<QWidget*>(o);
    if (e->type() == QEvent::Wheel && widget && !widget->hasFocus())
    {
        e->ignore();
        return true;
    }

    return QObject::eventFilter(o, e);
}

Then I set the focus policy of the problematic widget to StrongFocus, either at runtime or in Qt Designer. And then I install my event filter:

ui.comboBox->installEventFilter(new MouseWheelWidgetAdjustmentGuard(ui.comboBox));

Done. The MouseWheelWidgetAdjustmentGuard will be deleted automatically when the parent object - the combobox - is destroyed.

Lovash answered 26/5, 2016 at 12:22 Comment(3)
This seems dramatically easier than the above solutions, works great so far.Eblis
Works like a charm. Thank you.Declaratory
I ported this to Python under PyQt5 and it is working great (without the focusInEvent or focusOutEvent), see my answerAnarchism
W
6

Just to expand you can do this with the eventFilter instead to remove the need to derive a new QMySpinBox type class:

bool eventFilter(QObject *obj, QEvent *event)
{
    QAbstractSpinBox* spinBox = qobject_cast<QAbstractSpinBox*>(obj);
    if(spinBox)
    {
        if(event->type() == QEvent::Wheel)
        {
            if(spinBox->focusPolicy() == Qt::WheelFocus)
            {
                event->accept();
                return false;
            }
            else
            {
                event->ignore();
                return true;
            }
        }
        else if(event->type() == QEvent::FocusIn)
        {
            spinBox->setFocusPolicy(Qt::WheelFocus);
        }
        else if(event->type() == QEvent::FocusOut)
        {
            spinBox->setFocusPolicy(Qt::StrongFocus);
        }
    }
    return QObject::eventFilter(obj, event);
}
Witching answered 15/7, 2013 at 16:42 Comment(0)
A
4

This is my Python PyQt5 port of Violet Giraffe answer:


def preventAnnoyingSpinboxScrollBehaviour(self, control: QAbstractSpinBox) -> None:
    control.setFocusPolicy(Qt.StrongFocus)
    control.installEventFilter(self.MouseWheelWidgetAdjustmentGuard(control))

class MouseWheelWidgetAdjustmentGuard(QObject):
    def __init__(self, parent: QObject):
        super().__init__(parent)

    def eventFilter(self, o: QObject, e: QEvent) -> bool:
        widget: QWidget = o
        if e.type() == QEvent.Wheel and not widget.hasFocus():
            e.ignore()
            return True
        return super().eventFilter(o, e)

Anarchism answered 17/4, 2020 at 14:57 Comment(0)
E
3

With help from this post we cooked a solution for Python/PySide. If someone stumbles across this. Like we did :]

class HumbleSpinBox(QtWidgets.QDoubleSpinBox):
    def __init__(self, *args):
        super(HumbleSpinBox, self).__init__(*args)
        self.setFocusPolicy(QtCore.Qt.StrongFocus)

    def focusInEvent(self, event):
        self.setFocusPolicy(QtCore.Qt.WheelFocus)
        super(HumbleSpinBox, self).focusInEvent(event)

    def focusOutEvent(self, event):
        self.setFocusPolicy(QtCore.Qt.StrongFocus)
        super(HumbleSpinBox, self).focusOutEvent(event)

    def wheelEvent(self, event):
        if self.hasFocus():
            return super(HumbleSpinBox, self).wheelEvent(event)
        else:
            event.ignore()
Escargot answered 29/8, 2018 at 9:19 Comment(0)
W
0

Just to help anyone's in need, it lacks a small detail:

call focusInEvent and focusOutEvent from QSpinBox :

    void MySpinBox::focusInEvent(QFocusEvent* pEvent)
    {
        setFocusPolicy(Qt::WheelFocus);
        QSpinBox::focusInEvent(pEvent);
    }

    void MySpinBox::focusOutEvent(QFocusEvent*)
    {
        setFocusPolicy(Qt::StrongFocus);
        QSpinBox::focusOutEvent(pEvent);
    }
Westbrooks answered 5/9, 2013 at 13:39 Comment(1)
Hi jerome, welcome to Stack Overflow. Could you substantiate your answer a little to provide the reasons why it didn't work? This helps to explain to others (especially people who aren't so proficient in a particular language) why this solves the issue? Thanks!Gorman

© 2022 - 2024 — McMap. All rights reserved.