[Qt]自定義QStyle——實現QProgressBar自定義樣式


[Qt]自定義QStyle——實現QProgressBar自定義樣式

實現效果預覽

前言

​ 我們都知道Qt作為一個跨平台的桌面程序開發框架,其對樣式的匹配度非常的友好。正因為如此,使用自定義style開發出自己覺得看起來比較舒服的樣式對開發應用程序也是比較重要的。
​ 我們都知道Qt支持QSS來實現對程序中控件樣式的修改,雖然使用QSS修改程序樣式非常的方便,大多數人也會選擇使用他,但是久而久之,你就會發現使用QSS也會有一些弊端,比如:QSS語言單一古板,使用一種方式定義出來的QSS樣式表只有一種表現,另外程序中大量使用QSS就會顯得程序臃腫。因此,這里我們使用QStyle的方式修改程序樣式。
QStyle是Qt樣式的抽象基類,其衍生出來QCommonStyleQProxyStyle都部分效果的實現,但是具體效果並沒有做過多的定義。我們在程序中可以繼承QCommonStyle或者QProxyStyle實現自定義樣式,但是千萬不要使用QStyle繼承實現 樣式,當然你也可以不停我的勸,自己去實現,這樣的代碼量非常的龐大,基本上是從零開始。

一、介紹

​ 這里簡單介紹一下什么是QStyleQCommonStyleQProxyStyle

QStyle: 抽象基類,封裝了GUI的外觀,Qt中幾乎所有的部件都是用QStyle完成繪圖工作

QCommonStyle: 繼承自QStyle,封裝了GUI常見的外觀,實現了控件的部分外觀

QProxyStyle: 簡化了動態覆蓋QStyle元素的便利類,封裝了QStyle,可以動態覆蓋繪制或者其他行為

  • 以上摘自Qt官方文檔

​ 具體是什么意思呢?大家看了肯定雲里霧里,這里我解釋一下,QStyleQCommonStyle都是抽象類,需要用戶自己實現,當然既然你選擇使用這個類,你就要做好重新實現大量虛函數的准備,這些函數到底是什么,在哪里調用的,后面會說到。QProxyStyle是什么意思呢?為什么會有QProxyStyle這個類呢?我到底是繼承QCommonStyle還是QProxyStyle呢?相信大家肯定會有這樣的疑問,當初我剛接觸的時候也會有這樣的疑問,現在我告訴大家,QProxyStyle從名字中可以看到proxy代理,即代理樣式,它會預設出所有的代理樣式出來,如果你繼承QProxyStyle類實現自己的樣式,並且在使用的時候指定了代理樣式(構造函數中指定),那么除了自己定義的部分之外,其他的樣式都是代理樣式的,QStyle中有一個成員函數是proxy,返回代理樣式指針,一般會返回this指針,即如果繼承QCommonStyle自定義樣式,返回自身但不包括預設樣式,繼承QProxyStyle返回自身但是當控件樣式自定義未實現時,使用代理樣式。

二、分析

​ 由於我們只是實現QProgressBar的樣式,因此只需要繼承QCommonStyle即可。下面介紹一下Qt在實現時是怎么進行的。

1. QProgressBar中paintEvent的源碼

void QProgressBar::paintEvent(QPaintEvent *)
{
    QStylePainter paint(this);
    QStyleOptionProgressBar opt;
    initStyleOption(&opt);
    paint.drawControl(QStyle::CE_ProgressBar, opt);
    d_func()->lastPaintedValue = d_func()->value;
}

​ 細看源碼發現,首先調用style().drawControl()函數,並且傳遞的是QStyle::CE_ProgtressBar的參數。追根溯源,查看文檔發現CE_ProgressBar參數意思是一個QProgressBar,繪制CE ProgressBarGroove, CE ProgressBarContents和CE ProgressBarLabel。,還是沒法理解的話,查看QCommonStyle的源碼。

2. QCommonStyle中drawControl()函數的部分源碼

case CE_ProgressBar:
    if (const QStyleOptionProgressBar *pb
            = qstyleoption_cast<const QStyleOptionProgressBar *>(opt)) {
        QStyleOptionProgressBar subopt = *pb;
        subopt.rect = subElementRect(SE_ProgressBarGroove, pb, widget);
        proxy()->drawControl(CE_ProgressBarGroove, &subopt, p, widget);
        subopt.rect = subElementRect(SE_ProgressBarContents, pb, widget);
        proxy()->drawControl(CE_ProgressBarContents, &subopt, p, widget);
        if (pb->textVisible) {
            subopt.rect = subElementRect(SE_ProgressBarLabel, pb, widget);
            proxy()->drawControl(CE_ProgressBarLabel, &subopt, p, widget);
        }
    }
    break;
case CE_ProgressBarGroove:
    if (opt->rect.isValid())
        qDrawShadePanel(p, opt->rect, opt->palette, true, 1,
                        &opt->palette.brush(QPalette::Window));
    break;
case CE_ProgressBarLabel:
    if (const QStyleOptionProgressBar *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(opt)) {
        const bool vertical = pb->orientation == Qt::Vertical;
        if (!vertical) {
            QPalette::ColorRole textRole = QPalette::NoRole;
            if ((pb->textAlignment & Qt::AlignCenter) && pb->textVisible
                && ((qint64(pb->progress) - qint64(pb->minimum)) * 2 >= (qint64(pb->maximum) - qint64(pb->minimum)))) {
                textRole = QPalette::HighlightedText;
                //Draw text shadow, This will increase readability when the background of same color
                QRect shadowRect(pb->rect);
                shadowRect.translate(1,1);
                QColor shadowColor = (pb->palette.color(textRole).value() <= 128)
                   ? QColor(255,255,255,160) : QColor(0,0,0,160);
                QPalette shadowPalette = pb->palette;
                shadowPalette.setColor(textRole, shadowColor);
                proxy()->drawItemText(p, shadowRect, Qt::AlignCenter | Qt::TextSingleLine, shadowPalette,
                             pb->state & State_Enabled, pb->text, textRole);
            }
            proxy()->drawItemText(p, pb->rect, Qt::AlignCenter | Qt::TextSingleLine, pb->palette,
                         pb->state & State_Enabled, pb->text, textRole);
        }
    }
    break;
case CE_ProgressBarContents:
    if (const QStyleOptionProgressBar *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(opt)) {
        QRect rect = pb->rect;
        const bool vertical = pb->orientation == Qt::Vertical;
        const bool inverted = pb->invertedAppearance;
        qint64 minimum = qint64(pb->minimum);
        qint64 maximum = qint64(pb->maximum);
        qint64 progress = qint64(pb->progress);
        QTransform m;
        if (vertical) {
            rect = QRect(rect.y(), rect.x(), rect.height(), rect.width()); // flip width and height
            m.rotate(90);
            m.translate(0, -(rect.height() + rect.y()*2));
        }
        QPalette pal2 = pb->palette;
        // Correct the highlight color if it is the same as the background
        if (pal2.highlight() == pal2.window())
            pal2.setColor(QPalette::Highlight, pb->palette.color(QPalette::Active,
                                                                 QPalette::Highlight));
        bool reverse = ((!vertical && (pb->direction == Qt::RightToLeft)) || vertical);
        if (inverted)
            reverse = !reverse;
        int w = rect.width();
        if (pb->minimum == 0 && pb->maximum == 0) {
            // draw busy indicator
            int x = (progress - minimum) % (w * 2);
            if (x > w)
                x = 2 * w - x;
            x = reverse ? rect.right() - x : x + rect.x();
            p->setPen(QPen(pal2.highlight().color(), 4));
            p->drawLine(x, rect.y(), x, rect.height());
        } else {
            const int unit_width = proxy()->pixelMetric(PM_ProgressBarChunkWidth, pb, widget);
            if (!unit_width)
                return;
            int u;
            if (unit_width > 1)
                u = ((rect.width() + unit_width) / unit_width);
            else
                u = w / unit_width;
            qint64 p_v = progress - minimum;
            qint64 t_s = (maximum - minimum) ? (maximum - minimum) : qint64(1);
            if (u > 0 && p_v >= INT_MAX / u && t_s >= u) {
                // scale down to something usable.
                p_v /= u;
                t_s /= u;
            }
            // nu < tnu, if last chunk is only a partial chunk
            int tnu, nu;
            tnu = nu = p_v * u / t_s;
            if (nu * unit_width > w)
                --nu;
            // Draw nu units out of a possible u of unit_width
            // width, each a rectangle bordered by background
            // color, all in a sunken panel with a percentage text
            // display at the end.
            int x = 0;
            int x0 = reverse ? rect.right() - ((unit_width > 1) ? unit_width : 0)
                             : rect.x();
            QStyleOptionProgressBar pbBits = *pb;
            pbBits.rect = rect;
            pbBits.palette = pal2;
            int myY = pbBits.rect.y();
            int myHeight = pbBits.rect.height();
            pbBits.state = State_None;
            for (int i = 0; i < nu; ++i) {
                pbBits.rect.setRect(x0 + x, myY, unit_width, myHeight);
                pbBits.rect = m.mapRect(QRectF(pbBits.rect)).toRect();
                proxy()->drawPrimitive(PE_IndicatorProgressChunk, &pbBits, p, widget);
                x += reverse ? -unit_width : unit_width;
            }
            // Draw the last partial chunk to fill up the
            // progress bar entirely
            if (nu < tnu) {
                int pixels_left = w - (nu * unit_width);
                int offset = reverse ? x0 + x + unit_width-pixels_left : x0 + x;
                pbBits.rect.setRect(offset, myY, pixels_left, myHeight);
                pbBits.rect = m.mapRect(QRectF(pbBits.rect)).toRect();
                proxy()->drawPrimitive(PE_IndicatorProgressChunk, &pbBits, p, widget);
            }
        }
    }
    break;

函數實現很長,有性質可以看完,這里我總結一下,總的來說還是圍繞着幾個函數執行:

  • drawControl函數,一個繪制函數,具體繪制什么需要從參數屬性中獲取
  • drawPrimitive函數,同樣是繪制函數,根據指定參數繪制內容
  • subElementRect函數,返回子元素的QRect同樣的QCommenStyle不會過多幫助實現
  • pixelMetric函數,像素值,返回指定元素的像素值,QCommenStyle不會過多幫助實現

下面來看看屬性值,列舉了進度條的屬性值如下

  • PrimitiveElementdrawPrimitive函數的參數,其包含的進度條子元素為
    • PE_IndicatorProgressChunk:此元素表示進度覆蓋區域的元素,windows樣式是一小節一小節設定的
  • ControlElementdrawControl函數的參數,包含的進度條子元素為:
    • CE_ProgressBarContents:進度條內容部分,區別於文本部分,只包含進度區域
    • CE_ProgressBar:整個進度條部分,整個繪制QProgressBar的開始
    • CE_ProgressBarGroove:這個元素查看Qt源碼發現這個部分寬度為固定值1,而且從效果上看是介於內容和文本之間的部分
    • CE_ProgressBarLabel:進度條文本部分
  • SubElementsubElementRect函數的參數,包含進度條的子元素為:
    • SE_ProgressBarContents:返回進度條內容區域的QRect
    • SE_ProgressBarLabel:返回進度條文本區域的QRect
    • SE_ProgressBarGroove:返回介於文本和內容之間的部分,默認寬度為1

繪制進度條所需要的內容就是這些。下面列出進度條各區域的位置圖:

三、實現

1. 重新實現drawControl()函數部分內容

注意:由於QCommonStyle中已經實現了CE_ProgressBar的內容,如上面源碼所示,這里就不作實現

  • 繪制進度條整個內容部分,通過設置內容區域的rect為整個QProgressBar的區域,可以將Label區域與它重合實現字體在進度條上的效果
case CE_ProgressBarContents: {
    const QStyleOptionProgressBar *pb = qstyleoption_cast<const QStyleOptionProgressBar *>(opt);
    const bool vertial = pb->orientation == Qt::Vertical;
    QRect rect = pb->rect;
    int minimum = pb->minimum;
    int maximum = pb->maximum;
    int progress = pb->progress;
    QStyleOptionProgressBar pbBits = *pb;
    if (vertial) {
        pbBits.rect = QRect(rect.x(), rect.height() - int(rect.height() * double(progress) / (maximum-minimum)), rect.width(), int(rect.height() * double(progress) / (maximum-minimum)));
    } else {
        pbBits.rect = QRect(rect.x(), rect.y(), int(rect.width() * double(progress) / (maximum-minimum)), rect.height());
    }
    p->setBrush(QColor("#D3D3D3"));
    p->drawRoundedRect(rect, 8, 8);
    proxy()->drawPrimitive(PE_IndicatorProgressChunk, &pbBits, p, widget);
    return;
}
  • 繪制labelcontent之間的部分,由於labelcontent區域一致,這里就直接不管就行
case CE_ProgressBarGroove: {
    // 從源碼分析 這里寬度只有1
    p->setPen(Qt::transparent);
    p->setBrush(Qt::NoBrush);
    p->drawRect(opt->rect);
    return;
}
  • 繪制文本區域,這里的Rect是整個QProgressBar的區域,以便實現居中和字體漸變的效果

    這個效果主要是為了實現,進度條覆蓋文字時變色,通過觀察 fusion style的源碼發現它實現這一效果的方法時painter.setClipRect()這個函數,大家可以試試。

case CE_ProgressBarLabel: {
    const QStyleOptionProgressBar *pBarOpt = qstyleoption_cast<const QStyleOptionProgressBar *>(opt);
    QString text = QString("已完成").append(QString::number(double(pBarOpt->progress) / (pBarOpt->maximum-pBarOpt->minimum) * 100)).append("%");
    QFont font = p->font();
    bool vertical = pBarOpt->orientation == Qt::Vertical;
    font.setLetterSpacing(QFont::AbsoluteSpacing, 2);
    p->setFont(font);
    /* 字體矩形漸變色 */
    double mid = (double(pBarOpt->progress) / (pBarOpt->maximum-pBarOpt->minimum) > 0) ? double(pBarOpt->progress) / (pBarOpt->maximum-pBarOpt->minimum) : 0.001;
    mid = mid >= 1 ? 0.999 : mid;
    if (!vertical) {
        QLinearGradient textGradient(QPointF(pBarOpt->rect.left(), pBarOpt->rect.top()), QPointF(pBarOpt->rect.width(), pBarOpt->rect.top()));
        textGradient.setColorAt(0, Qt::white);
        textGradient.setColorAt(mid, Qt::white);
        textGradient.setColorAt(mid + 0.001, Qt::darkGray);
        textGradient.setColorAt(1, Qt::darkGray);
        p->setPen(QPen(QBrush(textGradient), 1));
        p->drawText(pBarOpt->rect, Qt::AlignCenter | Qt::TextSingleLine, text);
    } else {
        QLinearGradient textGradient(QPointF(pBarOpt->rect.left(), pBarOpt->rect.height()), QPointF(pBarOpt->rect.left(), pBarOpt->rect.top()));
        textGradient.setColorAt(0, Qt::white);
        textGradient.setColorAt(mid, Qt::white);
        textGradient.setColorAt(mid + 0.001, Qt::darkGray);
        textGradient.setColorAt(1, Qt::darkGray);
        p->setPen(QPen(QBrush(textGradient), 1));
        p->drawText(QRectF((pBarOpt->rect.width()-QFontMetricsF(p->font()).width("字"))/2, pBarOpt->rect.top(), QFontMetricsF(p->font()).width("字"), pBarOpt->rect.height()), Qt::AlignCenter | Qt::Tex
    }
    return;
}

2. 重新實現subElementRect()函數部分內容

  • 進度條內容區域rect,這里直接返回的整個區域的rect
case SE_ProgressBarContents: {
    r = widget->rect();
    break;
}
  • 進度條文本區域rect,同樣返回整個區域rect
case SE_ProgressBarLabel:
    r = subElementRect(QStyle::SE_ProgressBarContents, opt, widget);
    break;

3. 重新實現drawPrimitive()函數部分內容

  • 繪制當前進度區域,使用漸變方式進行
case PE_IndicatorProgressChunk: {
    QLinearGradient linear;
    linear.setStart(0, 0);
    linear.setFinalStop(widget->width(), widget->height());
    linear.setColorAt(0, QColor(255,182,193));
    linear.setColorAt(0.5, QColor(100,149,237));
    linear.setColorAt(1, QColor(255,222,173));
    painter->setPen(Qt::NoPen);
    painter->setBrush(linear);
    painter->drawRoundedRect(option->rect, 8, 8);
    return;
}

最后就能實現自定義QProgressBar的效果,同樣的方式,我們可以實現多種其他控件的樣式。本次只分享QProgressBar的樣式,感興趣的可以自己試試其他控件


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM