[Qt]自定義QStyle——實現QProgressBar自定義樣式
實現效果預覽
前言
我們都知道Qt
作為一個跨平台的桌面程序開發框架,其對樣式的匹配度非常的友好。正因為如此,使用自定義style開發出自己覺得看起來比較舒服的樣式對開發應用程序也是比較重要的。
我們都知道Qt
支持QSS
來實現對程序中控件樣式的修改,雖然使用QSS
修改程序樣式非常的方便,大多數人也會選擇使用他,但是久而久之,你就會發現使用QSS
也會有一些弊端,比如:QSS
語言單一古板,使用一種方式定義出來的QSS
樣式表只有一種表現,另外程序中大量使用QSS
就會顯得程序臃腫。因此,這里我們使用QStyle
的方式修改程序樣式。
QStyle
是Qt樣式的抽象基類,其衍生出來QCommonStyle
和QProxyStyle
都部分效果的實現,但是具體效果並沒有做過多的定義。我們在程序中可以繼承QCommonStyle
或者QProxyStyle
實現自定義樣式,但是千萬不要使用QStyle
繼承實現 樣式,當然你也可以不停我的勸,自己去實現,這樣的代碼量非常的龐大,基本上是從零開始。
一、介紹
這里簡單介紹一下什么是QStyle
、QCommonStyle
和QProxyStyle
QStyle
: 抽象基類,封裝了GUI的外觀,Qt中幾乎所有的部件都是用QStyle
完成繪圖工作
QCommonStyle
: 繼承自QStyle
,封裝了GUI常見的外觀,實現了控件的部分外觀
QProxyStyle
: 簡化了動態覆蓋QStyle
元素的便利類,封裝了QStyle
,可以動態覆蓋繪制或者其他行為
- 以上摘自Qt官方文檔
具體是什么意思呢?大家看了肯定雲里霧里,這里我解釋一下,QStyle
和QCommonStyle
都是抽象類,需要用戶自己實現,當然既然你選擇使用這個類,你就要做好重新實現大量虛函數的准備,這些函數到底是什么,在哪里調用的,后面會說到。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
不會過多幫助實現
下面來看看屬性值,列舉了進度條的屬性值如下
PrimitiveElement
:drawPrimitive
函數的參數,其包含的進度條子元素為PE_IndicatorProgressChunk
:此元素表示進度覆蓋區域的元素,windows樣式是一小節一小節設定的
ControlElement
:drawControl
函數的參數,包含的進度條子元素為:CE_ProgressBarContents
:進度條內容部分,區別於文本部分,只包含進度區域CE_ProgressBar
:整個進度條部分,整個繪制QProgressBar
的開始CE_ProgressBarGroove
:這個元素查看Qt源碼發現這個部分寬度為固定值1,而且從效果上看是介於內容和文本之間的部分CE_ProgressBarLabel
:進度條文本部分
SubElement
:subElementRect
函數的參數,包含進度條的子元素為: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;
}
- 繪制
label
和content
之間的部分,由於label
與content
區域一致,這里就直接不管就行
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
的樣式,感興趣的可以自己試試其他控件