用Qt实现一个动态缩放的滚动条
很早的时候做一个Qt项目,需求是实现一个滚动条动态变粗变细的效果。当时由于对Qt了解不多,就拒绝了。
最近忙完工作,突然想起这个需求,花点时间实现了一下,确实不难。以下是实际效果图(使用style sheet调整过样式,包括初始宽度):
示例代码已上传github
技术总结
实际上手实现的话,主要需要理解以下几点:
- QScrollArea与QScrollBar的关系
- 理解QWidget::sizeHint()
- 布局变化通知
1. QScrollArea与QScrollBar的关系
QScrollArea中的QSrollBar实际上并不直接的父子关系,QScrollBar存在于一个Widget的布局中,直接调用其setGeomerty()是不行的。而且,由于QScrollArea存在垂直与水平两个滚动条,其中一个的粗细变化都会影响另外一个的长度变化,进而影响滑块(Handle)。
2. 理解QWidget::sizeHint()
当创建并显示窗口时,总是需要一个值来决定显示多大,不显式指定时,Qt会通过QWidget::sizeHint()计算一个值,但不一定会使用,比如没有布局的QWidget会得到无效值。
需要注意的是,该值是建议值,窗口尺寸不绝对是该值。如果Widget存在布局,则是布局的sizeHint()与该建议值的综合结果,保证能够容纳建议布局且不小于该建议值。
类似地,存在QWidget::minimumSizeHint(),表示widget的最小建议尺寸,在不指定minimumSize的时候使用此值作为最小尺寸。
所以,通过重写QWidget::sizeHint()和QWidget::minimumSizeHint()可以实现某些特殊的需求。
Qt内部对sizeHint的计算是比较复杂的,需要根据内容、样式、布局等计算出合理的值,因此,重写QWidget::sizeHint()总是尽可能避免复杂的样式设定。我在工作中遇到过这样一个需求,一个列表当行数不大于N时,窗口刚好能完整显示;当大于N时则出现滚动条。有多种实现方案,我选择了QListView, 通过 “行高*行数” 来建议显示尺寸,但这就要求style sheet不能指定QListView的padding等,因为前面计算的建议值再需要加上QListView本身的padding、border等信息才行。
3. 布局变化通知
当一个Widget没有布局时,子控件的变化不会影响到该Widget的尺寸。更多时候,Widget需要布局管理子控件,其布局随着内容的变化需要不断调整。例如,重设QLabel的text,使内容超出原本布局,由于QLabel不会自动省略显示过长的内容,所以会触发父窗口调整尺寸,使其能容纳新的布局。也有部分Widget,比如QLineEdit,其默认的sizeHint并不依赖于其text的变化,仅会在样式变化时需要重新计算。
因此,要实现布局的动态调整,需要在手动更新sizeHint后通知布局。
Qt提供了QWidget::updateGeometry()来通知布局,该接口会触发QEvent::LayoutRequest,之后布局会重新调用QWidget::sizeHint()。
部分代码:
基本代码,为了简略,去掉了延时Timer。
//varAnima: 变量动画
//preferWidth: 临时记录动态的滚动条宽度
//_expandedWidth: 滚动条最大宽度
AnimatedScrollBar::AnimatedScrollBar(QWidget *parent):
QScrollBar(parent)
{
varAnima = new QVariantAnimation(this); //创建动画
varAnima->setDuration(100);
connect(varAnima, &QVariantAnimation::valueChanged, this, [this](const QVariant &val){
//valueChanged时,动画不一定在运行,需要约束
if(varAnima->state() == QAbstractAnimation::Running)
{
preferWidth = val.toInt();
updateGeometry(); //通知变化
}
});
}
QSize AnimatedScrollBar::sizeHint() const
{
QSize tmp = QScrollBar::sizeHint(); //样式指定的宽度值,可以通过默认的sizeHint获取
if(this->orientation() == Qt::Horizontal)
{
return QSize(tmp.width(), preferWidth); //仅改变宽度,实际由于布局的存在,长度值并不重要
}
return QSize(preferWidth, tmp.height());
}
bool AnimatedScrollBar::event(QEvent *e)
{
if(e->type() == QEvent::Polish)
{
//初始化preferWidth,也可以在第一次sizeHint()被调用时初始化
QSize tmp = QScrollBar::sizeHint();
preferWidth = this->orientation() == Qt::Horizontal ? tmp.height() : tmp.width();
}
else if(e->type() == QEvent::HoverEnter)
{
if(varAnima->state() == QAbstractAnimation::Running)
varAnima->stop();
varAnima->setStartValue(preferWidth);
varAnima->setEndValue(_expandedWidth);
varAnima->start();
}
else if(e->type() == QEvent::HoverLeave)
{
if(varAnima->state() == QAbstractAnimation::Running)
varAnima->stop();
QSize tmp = QScrollBar::sizeHint();
int normalWidth = this->orientation() == Qt::Horizontal ? tmp.height() : tmp.width();
varAnima->setStartValue(preferWidth);
varAnima->setEndValue(normalWidth);
varAnima->start();
}
return QScrollBar::event(e);
}