Search code examples
qtscrollfocus

Qt QTabWidget in QScroll area : concurrent scrolling


I have a QTabWidget some levels deeper in a QScrollArea. In Qt, there is the built-in possibility to use the mouse scroll wheel to cycle through tabs.

However, in my situation, this will also cause the QScrollArea to scroll up or down - which is super annoying.

This is undesired. I would like the tab bar to completely consume the scroll event and hide it entirely from the scroll area, to allow smooth, continuous scrolling through my tabs. Is that possible ?

Of note that I have other spin boxes and combo boxes that do show this behavior natively. I saw that they have the focus policy set to WheelFocus, which I now have set for my QTabWidget as well - to no avail.

I have also set the focus policy of the tab bar of the QTabWidget (myTabWidget->tabBar()) to WheelFocus via a line of code, also to no avail.


Solution

  • So I found that an answer could be that QTabBar does have the right kind of focus to be passed wheel events, but it overrides & reimplements QWidget::wheelEvent(QWheelEvent* event) in a way that does switch tabs as expected, but that doesn't QEvent::accept the event, i.e. consume it. Rather, on top of the tab-switching code, it simply calls the parent implementation in QWidget, which actually QEvent::ignores the QWheelEvent. Thus the QWheelEvent gets passed up in the parent hierarchy all the way up to my QScrollArea which also decides to scroll.

    (In comparison, things like QComboBoxes do override QWidget::wheelEvent(QWheelEvent* event) with a call to QEvent::accept at the end of their reimplementation. So this really seems to be the deciding factor.)

    One way to fix this would be to subclass QTabBar and override and reimplement QWidget::wheelEvent(QWheelEvent* event) with a call to QEvent::accept.

    But since I use the designer, I do not have access to the QTabBar itself, rather only to the QTabWidget, which internally creates and sets its QTabBar.

    I am however able to subclass QTabWidget and QEvent::accept the QWheelEvent there (and ask the designer to treat my QTabWidget as this new derived subclass), but it means that if I scroll anywhere on the QTabWidget then the event is consumed, whereas I only want this behavior on the QTabBar - else QEvent::ignore.

    So I added a small piece of code to find out if the event originated from a location that is within the tab bar.

    class ScrollableTabWidget : public QTabWidget
    {
        Q_OBJECT
    
    public:
    
        explicit ScrollableTabWidget(QWidget* parent = nullptr)
            : QTabWidget(parent) {}
    
        virtual ~ScrollableTabWidget() {}
    
        void wheelEvent(QWheelEvent * event) override
        {
            QWidget::wheelEvent(event);
            if(this->tabBar()->rect().contains(this->tabBar()->mapFromGlobal(event->globalPos())))
            {
                event->accept();
            }
        };
    };
    

    And then go to designer, right-click on the QTabWidget and ask "Use a placeholder for custom class" or something. It will ask for the class name and header file where the declarations are located, and then that's it ! This does the job.

    To be fair the event should be accepted natively in Qt ... I am using Qt 5.12.11 but even in Qt 6+ the problem is still there (see code, a call to QEvent::accept was inserted as part of a larger commit with more changes, but the method still finishes with QWidget::wheelEvent which means a call to QEvent::ignore ... I think, anyways).