Qt實現炫酷啟動圖-動態進度條

朝十晚八發表於2019-05-10

一、簡述

最近接到一個新需求,讓做一個動效進度條。

由於我們的產品比較大,在軟體啟動的時候會消耗比較長的時間,原生的進度條已經不能滿足我們的需求,這裡我們就需要一個會動的進度條,效果如下圖所示。

光效進度條主要是做了一個進度動畫,在已完成的部分上進行快速的迭代渲染,給使用者一種直觀感受,我們的軟體一直努力載入,它還活著。

Qt實現炫酷啟動圖-動態進度條

有了這個進度條之後,當我們的進度從40%到50%這個持續的過程中,介面再也不會出現假死的情況,是不是很完美呢。。。

下面我就來分析下這個動效進度條是怎麼做的

二、動效進度條

如效果圖所示,光效進度條不同於一般的進度條,他在基礎的任務進度之上還新增了一層光效,主要是想告訴使用者我們的軟體一直在努力執行,請在耐心的等待一下下。

我自己在做功能的時候,往往喜歡先做一個測試demo,然後在把做好的功能整合在正式環境,這個功能也不列外,如第一節中展示的效果圖,就是測試demo的樣子,雖然很醜,但是基礎功能是有的。

現在的很多軟體,在進度展示上都有了比較絢麗的效果,比如壓縮軟體,解壓檔案的時候都會有動效進度條,用過的同學應該都知道長啥樣,而我們的光效進度條跟這個效果差不多,除此之外我們還提供了另一種動效,延遲動效,他們兩個在一定程度上都展示了更友好的進度效果。

在開始分析功能前,首先我們先來考慮下我們的需求:動效進度條,也就是說在原來的進度條基礎上需要新增實時動畫,讓進度條看起來更炫酷一些,除了光效進度條以外,還有一種延遲到達進度條,也屬於動態進度條。

延遲動效、說直白一點兒就是延遲到達,當我們設定了進度從10%到20%時,程式模擬了一個漸進的過程,使用一個時間段走完了這10%的進度。

下面我們分別來介紹這兩種進度條的實現

實現炫酷的進度條我們可以從QWidget自定義開始寫,也就是說從頭開始寫,但通常我們不這樣幹,因為這樣可能會寫出無窮無盡的bug,而且現有的輪子已經很穩定了,為什麼還要造呢。

1、光效進度條

光效進度條我們使用了一個小技巧,採用一個簡單的辦法實現,我們的光效進度條控制元件繼承自Qt原生進度條類QProgressBar,在新類中我們只需要在Qt繪製完原生進度條之後,補畫動效即可。

a、paintEvent函式

paintEvent函式是Qt的繪製函式,當介面重新整理的時候,這個介面函式就會被呼叫,因此我們需要重寫這個介面,首先呼叫父視窗的繪製方法,然後我們在繪製我們自己的動效,程式碼如下所示

QProgressBar::paintEvent(event);
drawCache繪製動效

b、drawCache繪製動效

繪製動效的時候,我們需要知道動效的繪製區域,這個地方我們需要主動去解析qss的一些引數,Qt的style()->subElementRect這個介面剛好可以拿到我們需要的資訊

下面簡單描述下我們的實現流程

  • 首先我們獲取進度條的幾何大小和中間進度的幾何大小,這樣的話我們就可以計算出來各border的數值
  • 然後根據我們當前的value值就可以計算出進度條已經走過(就是值小於我們設定的區域)的幾何大小
  • 我們的光效將是跑在第二步計算出來的區域上,一直迴圈迭代
  • 記憶體裡我們維護一個cacheValue,這個值在每次介面重新整理的時候遞增,但是不能大於第二步的value值,cacheValue將是我們動效繪製的一個關鍵引數,他表示了動效繪製的長度
  • 構造一個漸變刷子,設定給QPainter
  • 繪製動效

上下大致描述了下繪製動效的一個流程,下面送上具體程式碼,由於篇幅原因,程式碼我進行了部分虛擬碼處理。

void GMPProgressBar::drawCache()
{
    QStyleOptionProgressBarV2 opt;
    QRect outerRect = style()->subElementRect(QStyle::SE_ProgressBarGroove, &opt, this);
    QRect innerRect = style()->subElementRect(QStyle::SE_ProgressBarContents, &opt, this);
    QMargins borders(構造一個QMargins);

    QRectF rect(動效繪製區域);

    if (m_iCacheValue != 0)
    {
        QPainter painter(this);
        QLinearGradient gradient(構造繪圖刷子);
        painter.setBrush(gradient);
        painter.drawRoundedRect(rect, 2, 2);
    }
}

c、定時重新整理

由於我們的動效是需要主動去重新整理的,因此我們需要宣告一個定時器,然後定時去重新整理,實現程式碼可能像下面這樣

connect(m_pCacheTimer, &QTimer::timeout, this, [this]{
        if (TM_CACHE == m_mode)
        {
            ++m_iCacheValue;
            repaint();
        }else
        {
            m_pCacheTimer->stop();
        }
    });

定時器只需要在我們第一次設定進度條值的時候啟動,或者當我們設定一個新的值,而定時器沒有啟動,我們就需要去啟用定時器。

TM_CACHE模式即是我們的動效模式,TM_SMOOTH模式則是我們的延遲到達模式

connect(this, &QProgressBar::valueChanged, [this](int value){//TM_CACHE模式下 啟動動畫時機
        if (!m_pCacheTimer->isActive() && value != 0 && TM_CACHE == m_mode)
        {
            m_pCacheTimer->start(m_iRefreshleveling);
        }
    });

動效進度條效果如下圖所示



Qt實現炫酷啟動圖-動態進度條


2、延遲到達進度條

動效進度條可能更適用於啟動介面,但是也有一些時候,我們可能需要更平緩的一個載入曲線,例如安裝軟體、解除安裝軟體的時候。

a、setValue

延遲到達進度條和動效進度條的實現方式就有所差別了,對於實現延遲到達進度條,我們這裡重寫了setValue函式,當外部呼叫該介面設定value值時,我們並沒有立即去設定當前值,而是使用了一個時間段去完成這個值得重新整理。

  • 外部呼叫setValue時,我們首先計算出我們應該繪製的最大寬度PixelMax、當前已經繪製到的最大寬度cacheValue和我們的步長
  • 設定定時器重新整理頻率,並重啟定時器
  • 定時器重新整理時,cacheValue自增我們的步長
  • 呼叫父類的QProgressBar::setValue介面設定值

b、定時器

延遲達到功能的的定時器和之前我們什麼的動效定時器可以混用一個,我們定時器重新整理的時候,針對不同的動畫模式,我們執行不同的的程式碼即可,實現程式碼可能像下面這樣

connect(m_pCacheTimer, &QTimer::timeout, this, [this]{
    if (TM_CACHE == m_mode)
    {
        ++m_iCacheValue;
        repaint();
    }
    else if (TM_SMOOTH == m_mode)
    {
        changeSmooth();
    }
    else
    {
        m_pCacheTimer->stop();
    }
});

延遲到達進度條效果如下圖所示



Qt實現炫酷啟動圖-動態進度條


3、介面說明

光效進度條類對外只暴露了3個介面,分別是設定動畫模式、動畫時長和重新整理頻率

特別需要注意的是,我們這裡重寫了父類的setValue介面,這意味著我們不能使用多型來操作這個介面

void setTransitionMode(TransitionMode mode);//設定動畫模式
void setSmoothDuration(int duration);//設定重新整理總時長 模式為TM_SMOOTH時有效
void setRefreshleveling(int rate);//設定重新整理頻率 每次更改TransitionMode之後會變為預設值

a、修改動畫模式

修改動畫模式的時候,我們需要清空記憶體中的所有資料,並把value值設為0。

void GMPProgressBar::setTransitionMode(TransitionMode mode)
{
    if (m_mode == mode)
    {
        return;
    }

    m_mode = mode;
    clearData();

    QProgressBar::setValue(0);
}

b、其他介面

設定重新整理時長和頻率介面都比較簡單,不做特別說明

特別注意:這個3個介面最好是在動畫啟動前設定,動畫開始後儘量不要去呼叫

三、啟動圖

第二節我們主要是講述了怎麼做一個動效進度條,這一節我們來做一個啟動圖頁面,把這個動效使用進去。

啟動圖不是我們主要分析的內容,這個我就簡單說下這個類的實現方式和一些藉口說明

1、實現思路

Qt已經給我們提供了一個QSplashScreen,但是使用起來還是特別有限,因此這裡我把Qt的原始碼直接進行了二次開發

  • 首先Qt的原生實現方式基本都被移植了出來
  • 啟動圖使用了簡單的上下佈局,上面是一張我自繪製的圖片,放在了一個QLabel上,下面是動效進度條
  • 自繪製的圖片上包括了,產品名稱、logo、背景圖等等

2、背景圖切換

當我們呼叫setPixmap設定背景圖時,如果我們指定了多張圖,我將會啟動一個定時器,在指定時長後重新構造一張大的背景圖,並新增到啟動視窗上

這裡主要說明下背景圖是怎麼構造出來的,程式碼如下所示

a、根據背景圖構造啟動圖大小,並移動到螢幕中間

m_currentPixmap = m_lstPixmaps.at(m_iCurrentIndex);

QRect size(QPoint(), m_currentPixmap.size()  / m_currentPixmap.devicePixelRatio());
size.setHeight(size.height() + StatusBarHeight);
setFixedSize(size.size());
m_pProgressBar->setFixedWidth(size.width() / 8 * 3);
move(QApplication::desktop()->screenGeometry().center() - size.center());
QPainter painter(&m_currentPixmap);
painter.drawPixmap(m_startPos, m_logo);

c、繪製標題欄

painter.save();
painter.setFont(m_titleFont);

QFontMetrics fontMetrics(m_titleFont);
int textWidth = fontMetrics.width(m_strWindowTitle);
int textHegith = m_logo.height();
QRect textTect = QRect(m_startPos + QPoint(13 + m_logo.width(), 0), QSize(textWidth, textHegith));
painter.drawText(textTect, m_strWindowTitle, QTextOption(Qt::AlignCenter));
painter.restore();

d、設定給QLabel背景圖

m_pWindowBackground->setPixmap(m_currentPixmap);

啟動圖的效果這裡就不在貼圖了,第三節上的兩個gif圖都是最終的啟動圖效果

四、測試

最後就是測試程式碼了,主要是模擬了程式的一個載入過程

1、構造啟動圖

首先我們構造一個啟動圖物件,並設定程式logo和動畫模式

GMPSplashScreen * screen = new GMPSplashScreen(QPixmap(":/splashScreen/start.png"));
screen->show();
screen->setLogo(QIcon("logo.ico").pixmap(48, 48));
screen->setTransitionMode(GMPProgressBar::TM_CACHE);

2、背景圖

設定背景圖,並設定背景圖更換時間間隔

QList<QPixmap> lstPixmap;
lstPixmap.append(QPixmap(":/splashScreen/start.png"));
lstPixmap.append(QPixmap(":/splashScreen/start.jpg"));
screen->setPixmap(lstPixmap, 2000);

3、其他資訊

設定程式的提示資訊和標題欄

screen->showMessage("Established connections", 0);
screen->setTitle(QStringLiteral("廣聯達BIM土建計量GTJ2018"));

4、事件迴圈

這裡寫了一個死迴圈,主要是為了模擬程式的一個載入過程,每隔10ms處理下介面重新整理事件

a.processEvents();

while (1)
{
    QTest::qSleep(10);
    a.processEvents();
}
splashScreen w;
w.show();

screen->finish(&w);

五、原始碼

需要原始碼的留郵箱,現在的csdn簡直太坑爹了。。。

相關文章