Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Customizing scrollbar over QListWidget

Tags:

qt

I'd like to implement my own semi-transparent scrollbar, that draws on top of the QListWidget instead of taking up permanent space in its viewport. I do not wish to use QML as my QListWidget and its dynamic content is already fully developed over 6 months now.

How can I achieve that. Stylesheets are useless for that purpose as they will not determine the positioning of the scrollbar. I'd like it to be on top of the QListWidget, not on its side, taking up its space.

I'm talking about something in the neighborhood of this:

enter image description here

Any hints as to how to do that will be appreciated.

like image 652
JasonGenX Avatar asked Apr 29 '13 18:04

JasonGenX


2 Answers

What you are trying to do is a perfect example of one thing that persistently annoys me about qt - if there is some graphical effect that Qt's designers haven't thought of, creating it on your own is a pain, constant fight against Qt, and usually ends with giving up anyway.

I suspect that you do it with small screens on your mind (cell phones? tablets?), so i guess there is no other way to solve this problem.

What I am trying here is hacky, but otherwise you would probably have to rewrite entire scrollbar yourself just to add those few missing details. My proposition is:

#ifndef MYSCROLLBAR_H
#define MYSCROLLBAR_H

#include <QScrollBar>

class MyScrollBar : public QScrollBar
{
    Q_OBJECT
public:
    explicit MyScrollBar(QWidget *parent = 0);

protected:
    void showEvent ( QShowEvent * event );
signals:

public slots:
   void updateMask();
};

#endif // MYSCROLLBAR_H

And in myscrollbar.cpp

#include "myscrollbar.h"

#include <QPaintEvent>
#include <QRegion>

#include <QStyleOptionSlider>

MyScrollBar::MyScrollBar(QWidget *parent) :
    QScrollBar(parent)
{
    connect(this, SIGNAL(valueChanged(int)), this, SLOT(updateMask()));
}

void MyScrollBar::updateMask(){
    QStyleOptionSlider opt;
    initStyleOption(&opt);

    QRegion r(style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this));
    r+= style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this);
    r+= style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this);
    setMask(r);
}

void MyScrollBar::showEvent ( QShowEvent * event ){
    QScrollBar::showEvent(event);
    updateMask();
}

Such scroller will be transparent (both visually and event-wise) in any of it's non-vital parts. It still creates some artifacts on widgets laying below it - I guess setMask() was never supposed to be used like this. To mitigate it you can connect valueChanged() signal to update() slot of the viewport of your list widget. This worked nice on my toy-example, but if you embed custom widgets in your list it might become unbearable to cope with. It might also run you into performance problems in case of more complex applications - especially if you write for mobile platforms.

Alternatively you can just "fork" entire QScrollBar class and simply modify it's paintEvent to use less subControls than SC_All - with additional setAttribute(Qt::WA_OpaquePaintEvent, false); in constructor it should provide visual transparency. Then you should also forward mouse events (if not hitting anything important) to your viewport of list widget (again, trouble with custom widgets in view).

Now what remains is writing your own layout class (or just manually positioning it) that will put both listview and scrollbar on one another in correct positions - QStackedLayout sounds nice, but it allows only one layer to be visible at any given time - clearly not what we are looking for.

Last step is switching off default scrollbars on view, and connecting signals/slots of the default (invisible) scrollbar to slots/signals of your scrollbar, to achieve effect of actual scrolling.

Shortly this will require a LOT of coding to get done. Are you sure that such a simple effect is worth it?

** EDIT: **

I create a layout class for stacking widgets on top of one another - this question gave me motivation to do it finally ;)

#ifndef STACKLAYOUT_H
#define STACKLAYOUT_H

#include <QLayout>

class StackLayout : public QLayout
{
    Q_OBJECT
public:
    StackLayout();
    explicit StackLayout(QWidget *parent);
    ~StackLayout();

    void addItem ( QLayoutItem * item );
    int count () const;
    Qt::Orientations expandingDirections () const;
    bool hasHeightForWidth () const;
    int heightForWidth ( int w ) const;
    QLayoutItem * itemAt ( int index ) const;
    bool isEmpty () const;
    QSize maximumSize () const;
    int minimumHeightForWidth ( int w ) const;
    QSize minimumSize () const;
    void setGeometry ( const QRect & r );
    QSize sizeHint () const;
    QLayoutItem * takeAt ( int index );

private:
     QList<QLayoutItem *> itemList;

};

#endif // STACKLAYOUT_H

And the stacklayout.cpp file:

StackLayout::StackLayout()
    :QLayout()
{}

StackLayout::StackLayout(QWidget *parent) :
    QLayout(parent)
{
}

StackLayout::~StackLayout(){
    QLayoutItem *item;
    foreach (item, itemList){
        delete item;
    }
}

void StackLayout::addItem ( QLayoutItem * item ){
    itemList.append(item);
}

int StackLayout::count () const{
    return itemList.count();
}

Qt::Orientations StackLayout::expandingDirections () const{
    Qt::Orientations result = 0;
    QLayoutItem *item;
    foreach (item, itemList){
        result = result | item->expandingDirections();
    }
    return result;
}

bool StackLayout::hasHeightForWidth () const{
    QLayoutItem *item;
    foreach (item, itemList){
        if (item->hasHeightForWidth())
            return true;
    }
    return false;
}

int StackLayout::heightForWidth ( int w ) const{
    int result = 0;
    QLayoutItem *item;
    foreach (item, itemList){
        if (item->hasHeightForWidth())
            result = qMax(result, item->heightForWidth(w));
    }
    return result;
}

QLayoutItem * StackLayout::itemAt ( int index ) const{
    if (index<itemList.count())
        return itemList[index];
    return 0;
}

bool StackLayout::isEmpty () const{
    QLayoutItem *item;
    foreach (item, itemList){
        if (!item->isEmpty())
            return false;
    }
    return true;
}

QSize StackLayout::maximumSize () const{
    QSize result=QLayout::maximumSize();
    QLayoutItem *item;
    foreach (item, itemList){
        result = result.boundedTo(item->maximumSize());
    }
    return result;
}

int StackLayout::minimumHeightForWidth ( int w ) const{
    int result = 0;
    QLayoutItem *item;
    foreach (item, itemList){
        if (item->hasHeightForWidth())
            result = qMax(result, item->minimumHeightForWidth(w));
    }
    return result;
}

QSize StackLayout::minimumSize () const{
    QSize result=QLayout::minimumSize();
    QLayoutItem *item;
    foreach (item, itemList){
        result = result.expandedTo(item->minimumSize());
    }
    return result;
}

void StackLayout::setGeometry ( const QRect & r ){
    QLayoutItem *item;
    foreach (item, itemList){
        item->setGeometry(r);
    }
}

QSize StackLayout::sizeHint () const{
    QSize result=QSize(0,0);
    QLayoutItem *item;
    foreach (item, itemList){
        result = result.expandedTo(item->sizeHint());
    }
    return result;
}

QLayoutItem * StackLayout::takeAt ( int index ){
    if (index < itemList.count())
        return itemList.takeAt(index);
    return 0;
}

Assuming you already have some nice transparent scrollbar, to insert it you would do:

QWidget* w = new QWidget();

StackLayout* sl = new StackLayout(w);
QListView* lv = new QListView(w);
sl->addWidget(lv);
QHBoxLayout* hbl = new QHBoxLayout();
sl->addItem(hbl);

TransparentScrollBar* tsc = new TransparentScrollBar(w);

hbl->addWidget(tsc,0);
hbl->insertStretch(0,1);
like image 191
j_kubik Avatar answered Nov 06 '22 13:11

j_kubik


Here is sample code for your questoin.

Not done: Mouse dragging of scroller

Done: Support of any mouse hover/leave events Support of scrolling Scroll bar is transparent for mouse events

It is good start point for any customization depending on you task. Usage:

GUI::MegaScrollBar *bar = new GUI::MegaScrollBar( ui->listWidget );
bar->resize( 40, 30 ); // First arg - width of scroller

MegaScrollBar.h

#ifndef MEGASCROLLBAR_H
#define MEGASCROLLBAR_H

#include <QWidget>
#include <QPointer>


class QAbstractItemView;
class QResizeEvent;

namespace GUI
{

    class MegaScrollBar
        : public QWidget
    {
        Q_OBJECT

    public:
        MegaScrollBar( QAbstractItemView *parentView );
        ~MegaScrollBar();

    private slots:
        void updatePos();

    private:
        bool eventFilter( QObject *obj, QEvent *event );
        void onResize( QResizeEvent *e );
        void paintEvent( QPaintEvent * event );
        void resizeEvent( QResizeEvent * event );

        QPointer< QAbstractItemView > m_view;
        QPointer< QWidget > m_scrollBtn;
    };

}


#endif // MEGASCROLLBAR_H

MegaScrollBar.cpp

#include "MegaScrollBar.h"

#include <QAbstractItemView>
#include <QEvent>
#include <QResizeEvent>
#include <QScrollBar>
#include <QDebug>
#include <QPainter>

#include "ScrollButton.h"


namespace GUI
{

    MegaScrollBar::MegaScrollBar( QAbstractItemView *parentView )
        : QWidget( parentView, Qt::FramelessWindowHint )
        , m_view( parentView )
    {
        Q_ASSERT( parentView );

        setAttribute( Qt::WA_TranslucentBackground );
        setAttribute( Qt::WA_TransparentForMouseEvents );

        m_scrollBtn = new ScrollButton( parentView );
        m_scrollBtn->setFixedSize( 20, 40 );

        m_view->installEventFilter( this );

        QScrollBar *sb = m_view->verticalScrollBar();
        connect( sb, SIGNAL( valueChanged( int ) ), this, SLOT( updatePos() ) );
    }

    MegaScrollBar::~MegaScrollBar()
    {
        removeEventFilter( m_view );
    }

    bool MegaScrollBar::eventFilter( QObject *obj, QEvent *event )
    {
        switch ( event->type() )
        {
        case QEvent::Enter:
            m_scrollBtn->show();
            break;

        case QEvent::Leave:
            m_scrollBtn->hide();
            break;

        case QEvent::Resize:
            onResize( static_cast< QResizeEvent * >( event ) );
            break;
        }

        return QWidget::eventFilter( obj, event );
    }

    void MegaScrollBar::onResize( QResizeEvent *e )
    {
        const int x = e->size().width() - width();
        const int y = 0;
        const int w = width();
        const int h = e->size().height();

        move( x, y );
        resize( w, h );

        updatePos();
    }

    void MegaScrollBar::updatePos()
    {
        QScrollBar *sb = m_view->verticalScrollBar();
        const int min = sb->minimum();
        const int val = sb->value();
        const int max = sb->maximum();
        const int x = pos().x() + ( width() - m_scrollBtn->width() ) / 2;

        if ( max == 0 )
        {
            m_scrollBtn->move( x, pos().y() );
            return ;
        }

        const int maxY = height() - m_scrollBtn->height();
        const int y = ( maxY * val ) / max;

        m_scrollBtn->move( x, y );
    }

    void MegaScrollBar::paintEvent( QPaintEvent * event )
    {
        Q_UNUSED( event );
        QPainter p( this );
        QRect rc( 0, 0, rect().width() - 1, rect().height() - 1 );

        // Draw any scroll background
        p.fillRect( rc, QColor( 255, 255, 200, 100 ) );
    }

    void MegaScrollBar::resizeEvent( QResizeEvent * event )
    {
        Q_UNUSED( event );
        updatePos();
    }

}

Preview:

Sample

It is possible to set up any widget for scroll button: Here is custom one: ScrollButton.h

#ifndef SCROLLBUTTON_H
#define SCROLLBUTTON_H

#include <QWidget>


namespace GUI
{

    class ScrollButton
        : public QWidget
    {
        Q_OBJECT

    public:
        ScrollButton( QWidget *parent );
        ~ScrollButton();

    private:
        void paintEvent( QPaintEvent * event );
    };

}


#endif // SCROLLBUTTON_H

ScrollButton.cpp

#include "ScrollButton.h"

#include <QPainter>
#include <QGraphicsOpacityEffect>
#include <QColor>


namespace GUI
{

    ScrollButton::ScrollButton( QWidget *parent )
        : QWidget( parent )
    {
        QGraphicsOpacityEffect *op = new QGraphicsOpacityEffect( this );
        op->setOpacity( 0.5 );
        setGraphicsEffect( op );
    }

    ScrollButton::~ScrollButton()
    {
    }

    void ScrollButton::paintEvent( QPaintEvent * event )
    {
        Q_UNUSED( event );

        // Draw any scroll button
        QPainter p( this );
        QRect rc( 5, 5, rect().width() - 6, rect().height() - 6 );

        p.fillRect( rc, QColor( 0, 0, 0, 255 ) );
    }

}

Please comment, if you can't handle mouse interaction.

like image 3
Dmitry Sazonov Avatar answered Nov 06 '22 12:11

Dmitry Sazonov