24

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.

Grant Limberg
  • 20,913
  • 11
  • 63
  • 84

7 Answers7

18

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.

emkey08
  • 5,059
  • 3
  • 33
  • 34
  • 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. – kkpattern Apr 03 '15 at 02:04
  • 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. – emkey08 Oct 18 '17 at 06:48
  • Well. Doing this with PySide turns the SpinBox to never have focus on a `wheelEvent`. overriding the `focusIn/OutEvents` did it! – ewerybody Aug 29 '18 at 09:16
16

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);
}
Matteo Italia
  • 123,740
  • 17
  • 206
  • 299
Marc Mutz - mmutz
  • 24,485
  • 12
  • 80
  • 90
  • 1
    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. – Grant Limberg Apr 28 '11 at 16:38
  • Then you also need to filter the wheel events from reaching `QSpinBox`. I've extended my answer. – Marc Mutz - mmutz Apr 28 '11 at 17:03
  • 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. – Grant Limberg Apr 28 '11 at 17:15
  • @Grant: have you tried just asking the spinbox for hasFocus() in the event filter? – Marc Mutz - mmutz Apr 28 '11 at 17:55
  • @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. – Grant Limberg Apr 28 '11 at 18:04
  • @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. – Marc Mutz - mmutz Apr 28 '11 at 18:18
  • 1
    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. – gfrigon Dec 05 '13 at 21:30
  • 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. – Tim MB Jan 17 '14 at 15:50
  • -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. – Valentin H Feb 10 '15 at 11:29
  • @ValentinHeinitz: Maybe you could edit the code instead of downvoting? Nevermind... – Marc Mutz - mmutz Feb 10 '15 at 14:07
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.

Violet Giraffe
  • 32,368
  • 48
  • 194
  • 335
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);
}
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)

Chnossos
  • 9,971
  • 4
  • 28
  • 40
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()
Chnossos
  • 9,971
  • 4
  • 28
  • 40
ewerybody
  • 1,443
  • 16
  • 29
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);
    }
Ben
  • 51,770
  • 36
  • 127
  • 149
jerome
  • 1
  • 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! – Qantas 94 Heavy Sep 05 '13 at 13:57