[Qt]自定义QStyle——实现QProgressBar自定义样式

Posted during

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[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的样式,感兴趣的可以自己试试其他控件



以上是关于[Qt]自定义QStyle——实现QProgressBar自定义样式的主要内容,如果未能解决你的问题,请参考以下文章

自定义QT窗口部件外观之QStyle

定制QSpinBox外观

QT界面 理解QStyle和QStyleOption以及QStyleFactory

用QT创建新风格: QStyle

如何从 QStyle 获取 QTreeView 的标识宽度

QT常见面试题,基础知识偏多