Qt自帶的的列表控制元件是不能平滑滾動的,但如果滾動速度快的話很容易引起視線丟失,體驗效果很差。本篇主要講述如何在Qt中對列表控制元件加入平滑滾動。文中以QScrollArea
控制元件為例,其他控制元件方法一樣。
原理
Qt的列表控制元件中,有以下兩個介面:
void QAbstractScrollArea::setHorizontalScrollBar(QScrollBar *scrollBar);
void QAbstractScrollArea::setVerticalScrollBar(QScrollBar *scrollBar);
顯然上述兩個介面的作用是設定控制元件的橫縱兩個滾動條,以指標形式傳入,我們將以此來實現控制頁面的滾動。傳入滾動條物件後,我們就可以使用setValue()
來間接控制頁面的滾動了。然後再使用QPropertyAnimation
類來實現滾動的效果。
實現
一、自定義滾動條控制元件
需要實現兩個功能:
- 當使用
setValue()
對滾動條的滑塊進行移動時,滑塊會在一個時間段內以某種規律連續的移動到目標位置,而不是瞬間移動。 - 新增一個槽函式
void scroll(int value)
,實現傳入一個數字後相對滾動指定距離。如:scroll(100)就是向下滾動100個單位。
標頭檔案:
#ifndef SMOOTHSCROLLBAR_H
#define SMOOTHSCROLLBAR_H
#include <QScrollBar>
#include <QPropertyAnimation>
class SmoothScrollBar : public QScrollBar
{
Q_OBJECT
public:
SmoothScrollBar(QWidget *parent=nullptr);
private:
//這裡重寫滑鼠事件的目的是在手動點選或拖動滾動條時更新m_targetValue_v變數,並且在拖動時立即結束滾動的動畫。
//這裡如果不明白作用,可以先註釋掉看看手動拖動滾動條時對動畫有什麼影響。
void mousePressEvent(QMouseEvent *) override;
void mouseReleaseEvent(QMouseEvent *) override;
void mouseMoveEvent(QMouseEvent *) override;
QPropertyAnimation *m_scrollAni; //用來實現動畫
int m_targetValue_v; //用來記錄目標位置的變數
public slots:
void setValue(int value); //重寫的setValue槽函式,實現動畫效果
void scroll(int value); //新增相對滾動的槽函式,value為滾動距離的向量表示
signals:
};
#endif // SMOOTHSCROLLBAR_H
原始檔:
#include "smoothscrollbar.h"
#include <QWheelEvent>
SmoothScrollBar::SmoothScrollBar(QWidget* parent):QScrollBar(parent)
{
m_scrollAni=new QPropertyAnimation;
m_scrollAni->setTargetObject(this);
m_scrollAni->setPropertyName("value");
m_scrollAni->setEasingCurve(QEasingCurve::OutQuint); //設定動畫曲線,在Qt文件中有詳細的介紹
m_scrollAni->setDuration(800); //設定動畫時間,數值越小播放越快
m_targetValue_v=value(); //將m_targetValue_v初始化
}
void SmoothScrollBar::setValue(int value)
{
m_scrollAni->stop();//停止現在的動畫,防止出現衝突
m_scrollAni->setStartValue(this->value()); //設定動畫滾動的初始值為當前位置
m_scrollAni->setEndValue(value); //設定動畫的結束位置為目標值
m_scrollAni->start(); //開始動畫
}
void SmoothScrollBar::scroll(int value)
{
m_targetValue_v-=value; //將目標值和相對位置進行運算
setValue(m_targetValue_v); //開始動畫
}
void SmoothScrollBar::mousePressEvent(QMouseEvent *e)
{
//當使用滑鼠操作滾動條時,不會重新整理m_targetValue_v的值,因而需要重寫事件,對其進行重新整理。
m_scrollAni->stop();
QScrollBar::mousePressEvent(e);
m_targetValue_v=value();
}
void SmoothScrollBar::mouseReleaseEvent(QMouseEvent *e)
{
m_scrollAni->stop();
QScrollBar::mouseReleaseEvent(e);
m_targetValue_v=value();
}
void SmoothScrollBar::mouseMoveEvent(QMouseEvent *e)
{
m_scrollAni->stop();
QScrollBar::mouseMoveEvent(e);
m_targetValue_v=value();
}
二、自定義列表控制元件
將列表的滾動條替換為我們剛剛自定義的滾動條
標頭檔案:
#ifndef SMOOTHSCROLLAREA_H
#define SMOOTHSCROLLAREA_H
#include <QWidget>
#include <QScrollArea>
#include "smoothscrollbar.h"
class SmoothScrollArea : public QScrollArea
{
Q_OBJECT
public:
explicit SmoothScrollArea(QWidget *parent = nullptr);
private:
SmoothScrollBar* vScrollBar; //縱向滾動條
void wheelEvent(QWheelEvent* e); //捕獲滑鼠滾輪事件
};
#endif // SMOOTHSCROLLAREA_H
原始檔:
#include "smoothScrollArea.h"
#include <QVBoxLayout>
#include <QLabel>
#include <QWheelEvent>
#include <QDebug>
SmoothScrollArea::SmoothScrollArea(QWidget *parent) : QScrollArea(parent)
{
auto layout = new QVBoxLayout;
vScrollBar=new SmoothScrollBar();
vScrollBar->setOrientation(Qt::Orientation::Vertical); //將滾動條設定為縱向
QWidget* w=new QWidget; //主體Widget
for (int i=0;i<200 ;i++ ) { //在w中加入200個label,用來測試滾動
QFont font;
font.setPointSize(i+1);
auto a=new QLabel(QString::number(i));
a->setFont(font);
layout->addWidget(a);
}
setVerticalScrollBar(vScrollBar); //設定縱向滾動條
w->setLayout(layout); //設定佈局
setWidget(w); //設定widget
}
void SmoothScrollArea::wheelEvent(QWheelEvent *e)
{
//當捕獲到事件後,呼叫相對滾動的槽函式
vScrollBar->scroll(e->angleDelta().y());
}
到此為止,SmoothScrollArea
類便可以支援縱向的平滑滾動。其他的列表控制元件方法一致。
三、測試效果
將SmoothScrollArea
列表控制元件加入到主視窗後,執行即可。
補充
在此之前參考過deepin-launcher
的小視窗模式列表程式碼,deepin的平滑滾動策略存在缺陷,導致體驗較差。這裡我詳細說明一下這一簡單的問題,非deepin使用者或開發者可以到此為止了。
/**
* @brief AppListView::wheelEvent 滑鼠滑輪事件觸發滑動區域控制元件動畫
* @param e 滑鼠滑輪事件指標物件
*/
void AppListView::wheelEvent(QWheelEvent *e)
{
if (e->orientation() == Qt::Horizontal)
return;
int offset = -e->delta();
m_scrollAni->stop();
m_scrollAni->setStartValue(verticalScrollBar()->value());
m_scrollAni->setEndValue(verticalScrollBar()->value() + offset * m_speedTime);
m_scrollAni->start();
}
由程式碼中可以看到,首先他遮蔽了橫向滑動的事件,這個主要應對觸控板的一些問題。
offset
為滾動的距離,每次滾動之前先停止上次動畫(如果還沒有結束的話),然後在以當前位置為起始位置,以相對offset的位置為結束位置,這就會導致一個問題。
假設有兩個連續的滾動事件被觸發,上邊這個函式就會執行兩次。這裡會出現兩種情況:
- 兩次連續的事件時間間隔比較大,滑鼠滾輪速度比較慢。
- 兩次連續事件時間間隔很小,小於動畫時長,滑鼠滾動速度很快。
第一種情況下,deepin的這個方案並沒有什麼問題,兩次動畫之間互不干擾。但是第二種情況就會發現問題,假設動畫時長為800ms,當第一次滾動事件觸發後,隨後800ms的時間中,滾動條將相應的向目標位置滾動,但是當第二次事件在第一次的動畫還未結束時到來的話,第一次的動畫將被打斷,也就是隻滾動了一部分就結束了,因此,當連續的快速滾動事件結束後,實際滾動的距離要遠小於期望的距離。
這個問題在本文中的解決方案時建立一個目標位置的變數。此變數用於記錄滾動所期望的位置,不會導致滾動失真。
大致如下:
設記錄目標位置的變數為a
,a的值將被初始化為滾動條當前的value,此後,當滑鼠滾動事件被觸發時,首先將a的值通過和滾動距離的計算變為新的位置,此時當前位置與a的值將不再相等,然後在通過動畫將結束位置定為a。這樣處理的好處,通過上邊的方法分析如下:
首先第一種情況是沒有區別的,來看第二種。同樣假設動畫時方案長為800ms,當第一次滾動事件發生後,a將被計算為要滾動的目標位置,隨後的800ms將是動畫的執行過程,當這個過程還未結束時,第二次滾動又將a的值通過和滾動距離的計算,變為一個新的位置,再由動畫去執行。這裡注意,a在兩次變化中,第一次的距離並未丟失,兩次距離相加,當連續的快速滾動事件結束後,實際滾動的距離等於所期望的距離。
deepin那種方案所導致的現象
- 快速滾動滾輪並不能讓列表的滾動速度便快,甚至還可能不如滾輪滾的慢一點。
- 滾動的動畫將會在一些情況下看起來不流暢
希望deepin能採納文中的方案。
文中程式碼下載地址:https://maicss.lanzoui.com/iQHCHswqm6j