这段时间研究了一下,qt的样式源码。同时也顺便自己做了一个样式。为了使大家能够看下去,先贴一贴效果。
以上效果,看似好像用qss采用setStyleSheet()的方式也可以更改;但结果证明是不行的,采用qss的方式只能设置一下颜色,皮肤。
而上面的菜单背景为透明,子菜单的的三角标志已经是圆了,所以靠样式表是不行的。
QStyle接口实现了qt在各种不同平台之间的各种控件的基本外观,查看QStyle源码可以看见定义了非常多的枚举变量;主要有以下几类
ComplexControl、ControlElement、PrimitiveElement、PixelMetric、ContentsType、SubElement、SubControl。我们的QPusButton,QLlineEdit,QMenu等待
都是靠这些枚举变量,样式层里分成不同的小块,在各自的接口函数里面用QPainter一点一点绘制出来的。主要的接口函数有这几个
virtual void
drawComplexControl(ComplexControl control, const QStyleOptionComplex *option,
QPainter *painter, const QWidget *widget = Q_NULLPTR) const ; //绘制复杂的控件,具体是哪些呢,这个就是通过枚举ComplexControl来控件了,通过
帮助文档我们可以看出该函数处理的控件元素有这些CC_SpinBox、CC_ComboBox、CC_ScrollBar、CC_Slider、CC_ToolButton、CC_TitleBar、CC_GroupBox、
CC_Dial、CC_MdiControls。
virtual void
drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = Q_NULLPTR) const ;
//可处理的控件元素是这些CE_PushButton、CE_PushButtonBevel、CE_PushButtonLabel、CE_DockWidgetTitle...这个枚举有很多,就不写完了。可以
看出它画的是一些简单一些的控件,像按钮、进度条等;而这些又被分成几个元素来绘制,如按钮又分为CE_PushButtonBevel、CE_PushButtonLabel。
然后在细分的枚举里面再去绘制更具体的部分
virtual void
drawPrimitive(PrimitiveElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget = Q_NULLPTR) const;
//可处理的元素非常多PE_PanelButtonCommand、PE_FrameDefaultButton、PE_PanelButtonBevel;从文档可以看出它画的是一些不可再分的原子组件
如按钮的背景面板,QLineEdit的背景面板等。
virtual int
pixelMetric(PixelMetric metric, const QStyleOption *option = Q_NULLPTR, const QWidget *widget = Q_NULLPTR) const;
//返回对应枚举的像素公制,通常做为控件的线宽,文字和图案,图案与边角的margin值;可处理的对象有PM_ButtonMargin、PM_ButtonShiftVertical、
等等。
virtual void
polish(QWidget *widget) 在控件初始时被调用,只有一次调用。
virtual void
polish(QPalette &palette) 在控件初始化被调用,只调用一次。
virtual void
unpolish(QWidget *widget) 在当前样式被卸载时调用,没被卸载时,在polish(QWidget*widget)中一般会启用WA_Hover,就是当鼠标滑过控件时会重绘。
当样式卸载时,就在这里将这个属性失效。
virtual QRect
subControlRect(ComplexControl control, const QStyleOptionComplex *option, SubControl subControl, const QWidget *widget = Q_NULLPTR) const ;
//返回一个复杂控件ComplexControl的子控件SubControl的位置,通常在drawComplexControl函数中被调用,用于获取子控件的位置。所以它控制着复杂控件
的布局
virtual QRect
subElementRect(SubElement element, const QStyleOption *option, const QWidget *widget = Q_NULLPTR) const ;
//返回一个子元素内容位置,如QPushButton的文本显示区,QLineEdit的文本区,各种控件的焦点区域等。
virtual int
styleHint(StyleHint hint, const QStyleOption *option = Q_NULLPTR, const QWidget *widget = Q_NULLPTR, QStyleHintReturn *returnData = Q_NULLPTR) const ;
//当状态发生变化时,反回一个让你感觉状态变化了的实际对应控件的对应某个参数的int值。为bool时一般表示,是否具有某个属性
;为整型时,表示一个值,如透明度等。
了解了以上这些接口后,我们再来看一段QStyle的实现源代码QCommonStyle的drawPrimitivet的源码
void QCommonStyle::drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p,
const QWidget *widget) const
{
Q_D(const QCommonStyle);
switch (pe) {
case PE_FrameButtonBevel:
case PE_FrameButtonTool:
qDrawShadeRect(p, opt->rect, opt->palette,
opt->state & (State_Sunken | State_On), 1, 0);
break;
case PE_PanelButtonCommand:
case PE_PanelButtonBevel:
case PE_PanelButtonTool:
case PE_IndicatorButtonDropDown:
qDrawShadePanel(p, opt->rect, opt->palette,
opt->state & (State_Sunken | State_On), 1,
&opt->palette.brush(QPalette::Button));
break;
case PE_IndicatorViewItemCheck:
proxy()->drawPrimitive(PE_IndicatorCheckBox, opt, p, widget);
break;
case PE_IndicatorCheckBox:
if (opt->state & State_NoChange) {
p->setPen(opt->palette.foreground().color());
p->fillRect(opt->rect, opt->palette.brush(QPalette::Button));
p->drawRect(opt->rect);
p->drawLine(opt->rect.topLeft(), opt->rect.bottomRight());
} else {
qDrawShadePanel(p, opt->rect.x(), opt->rect.y(), opt->rect.width(), opt->rect.height(),
opt->palette, opt->state & (State_Sunken | State_On), 1,
&opt->palette.brush(QPalette::Button));
}
break;
case PE_IndicatorRadioButton: {
QRect ir = opt->rect;
p->setPen(opt->palette.dark().color());
p->drawArc(opt->rect, 0, 5760);
if (opt->state & (State_Sunken | State_On)) {
ir.adjust(2, 2, -2, -2);
p->setBrush(opt->palette.foreground());
bool oldQt4CompatiblePainting = p->testRenderHint(QPainter::Qt4CompatiblePainting);
p->setRenderHint(QPainter::Qt4CompatiblePainting);
p->drawEllipse(ir);
p->setRenderHint(QPainter::Qt4CompatiblePainting, oldQt4CompatiblePainting);
}
break; }
void QCommonStyle::drawComplexControl(ComplexControl cc, const QStyleOptionComplex *opt,
QPainter *p, const QWidget *widget) const
{
switch (cc) {
#ifndef QT_NO_SLIDER
case CC_Slider:
if (const QStyleOptionSlider *slider = qstyleoption_cast(opt)) {
if (slider->subControls == SC_SliderTickmarks) {
int tickOffset = proxy()->pixelMetric(PM_SliderTickmarkOffset, slider, widget);
int ticks = slider->tickPosition;
int thickness = proxy()->pixelMetric(PM_SliderControlThickness, slider, widget);
int len = proxy()->pixelMetric(PM_SliderLength, slider, widget);
int available = proxy()->pixelMetric(PM_SliderSpaceAvailable, slider, widget);
int interval = slider->tickInterval;
if (interval <= 0) {
interval = slider->singleStep;
if (QStyle::sliderPositionFromValue(slider->minimum, slider->maximum, interval,
available)
- QStyle::sliderPositionFromValue(slider->minimum, slider->maximum,
0, available) < 3)
interval = slider->pageStep;
}
if (!interval)
interval = 1;
int fudge = len / 2;
int pos;
在后面的代码中,还会根据需要的原子控件,构造一个枚举调用drawPrimitivet函数;当然也可以在这里用QPainter画,也就是给你一个复杂控件的枚举
和一片区域和参数,在这片区域上你想怎么画就怎么画,只不有些原子组件和子控件可以调用其它接口。
void QCommonStyle::drawControl(ControlElement element, const QStyleOption *opt,
QPainter *p, const QWidget *widget) const
{
Q_D(const QCommonStyle);
switch (element) {
case CE_PushButton:
if (const QStyleOptionButton *btn = qstyleoption_cast(opt)) {
proxy()->drawControl(CE_PushButtonBevel, btn, p, widget);
QStyleOptionButton subopt = *btn;
subopt.rect = subElementRect(SE_PushButtonContents, btn, widget);
proxy()->drawControl(CE_PushButtonLabel, &subopt, p, widget);
if (btn->state & State_HasFocus) {
QStyleOptionFocusRect fropt;
fropt.QStyleOption::operator=(*btn);
fropt.rect = subElementRect(SE_PushButtonFocusRect, btn, widget);
proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, p, widget);
}
}
break;
之间加东西,就可以在这个接口里面用QPainter画了,当然全部都在这里画也是可以的。
void QCommonStyle::polish(QWidget *widget)
{
QStyle::polish(widget);
}
QCommonStyle里没有实现这个接口,因为QCommonStyle是QStyle接口实现的第一层,实现了一些公有的样式,在QWindowsXpStyle里我们可以看到这样一行代码
widget->setAttribute(Qt::WA_Hover);//意思就是当鼠标进入该控件时,该控件就重绘
了解完了上面这些后,对qt的样式机制应该明白了不少吧。下面来看一个最简单的QPushButton实现的源代码
我们直接进入paintEvent绘图事件
void QPushButton::paintEvent(QPaintEvent *)
{
QStylePainter p(this); //指定绘图设备会this
QStyleOptionButton option; //初始化一个样式按钮数据类,包含了按钮的各种参数
initStyleOption(&option); //给按钮参数赋值
p.drawControl(QStyle::CE_PushButton, option); //调用QPushButton采用的style对象的drawControl函数画一个枚举为CE_PushButton的控件,也就是按钮啦
}
void QPushButton::initStyleOption(QStyleOptionButton *option) const
{
if (!option)
return;
Q_D(const QPushButton);
option->initFrom(this);
option->features = QStyleOptionButton::None;
if (d->flat)
option->features |= QStyleOptionButton::Flat;
#ifndef QT_NO_MENU
if (d->menu)
option->features |= QStyleOptionButton::HasMenu;
#endif
if (autoDefault())
option->features |= QStyleOptionButton::AutoDefaultButton;
if (d->defaultButton)
option->features |= QStyleOptionButton::DefaultButton;
if (d->down || d->menuOpen)
option->state |= QStyle::State_Sunken;
if (d->checked)
option->state |= QStyle::State_On;
if (!d->flat && !d->down)
option->state |= QStyle::State_Raised;
option->text = d->text;
option->icon = d->icon;
option->iconSize = iconSize();
}
还是简单控件(可查看QStyle接口的枚举得知)调用对应的接口函数,最后就进入了QStyle的绘画接口了,在接口中根据传入的枚举值和参数,使用QPainter来
画出控件。
但真实的按钮并不是这么简单,因为前面我们看的是QCommonStyle,在windows平台,Widget默认使用的是QWindowsVistaStyle,可以使用qDebug() << object->style()
打印出来,QWindowsVistaStyle是通过几次继承从QCommonStyle继承下来的;QStyle的接口是虚函数,所以具体是怎样展示的还要看它使用的样式的实现接口。
我们不难看出windows平台上面的按钮有一个动画过程,当鼠标放上去,点击和移开都可以发现。这个动画的影子在QWindowsVistaStyle中就能看到影子,就是在drawControl
的接口函数中
void QWindowsVistaStyle::drawControl(ControlElement element, const QStyleOption *option,
QPainter *painter, const QWidget *widget) const
{
QWindowsVistaStylePrivate *d = const_cast(d_func());
if (!QWindowsVistaStylePrivate::useVista()) {
QWindowsStyle::drawControl(element, option, painter, widget);
return;
}
bool selected = option->state & State_Selected;
bool pressed = option->state & State_Sunken;
bool disabled = !(option->state & State_Enabled);
int state = option->state;
int themeNumber = -1;
QRect rect(option->rect);
State flags = option->state;
int partId = 0;
int stateId = 0;
if (d->transitionsEnabled() && canAnimate(option))
{
if (element == CE_PushButtonBevel) {
QRect oldRect;
QRect newRect;
QObject *styleObject = option->styleObject;
int oldState = styleObject->property("_q_stylestate").toInt();
oldRect = styleObject->property("_q_stylerect").toRect();
newRect = option->rect;
styleObject->setProperty("_q_stylestate", (int)option->state);
styleObject->setProperty("_q_stylerect", option->rect);
bool wasDefault = false;
bool isDefault = false;
if (const QStyleOptionButton *button = qstyleoption_cast(option)) {
wasDefault = styleObject->property("_q_isdefault").toBool();
isDefault = button->features & QStyleOptionButton::DefaultButton;
styleObject->setProperty("_q_isdefault", isDefault);
}
bool canAnimate(const QStyleOption *option) {
return option
&& option->styleObject
&& !option->styleObject->property("_q_no_animation").toBool();
}
这就显而易见了,如果我们这样QPushButton *btn = new QPushButton;btn->setProperty("_q_noanimation",false)这样你就会发现按钮不会有动画过程了
这个d为一个QWindowsVistaStylePrivate类,在其定义里未找到该函数定义,该类经由QCommonStylePrivate继承,在QCommonStylePrivate中我们找到了定义
void QCommonStylePrivate::startAnimation(QStyleAnimation *animation) const
{
Q_Q(const QCommonStyle);
stopAnimation(animation->target());
q->connect(animation, SIGNAL(destroyed()), SLOT(_q_removeAnimation()), Qt::UniqueConnection);
animations.insert(animation->target(), animation);
animation->start();
}
而启动动画又是怎样的呢,这又要看QStyleAnimaion类了。
QStyleAnimation类未找到start()函数定义,它继承自QAbstractAnimation,在QAbstractAnimation中start()函数定义如下
void QAbstractAnimation::start(DeletionPolicy policy)
{
Q_D(QAbstractAnimation);
if (d->state == Running)
return;
d->deleteWhenStopped = policy;
d->setState(Running);
}
最后调用了setState(),跟踪其中一段结果如下
case QAbstractAnimation::Running:
{
// this ensures that the value is updated now that the animation is running
if (oldState == QAbstractAnimation::Stopped) {
if (isTopLevel) {
// currentTime needs to be updated if pauseTimer is active
QAnimationTimer::ensureTimerUpdate();
q->setCurrentTime(totalCurrentTime);
}
}
}
break;
void QAbstractAnimation::setCurrentTime(int msecs)
{
Q_D(QAbstractAnimation);
msecs = qMax(msecs, 0);
// Calculate new time and loop.
int dura = duration();
int totalDura = dura <= 0 ? dura : ((d->loopCount < 0) ? -1 : dura * d->loopCount);
if (totalDura != -1)
msecs = qMin(totalDura, msecs);
d->totalCurrentTime = msecs;
// Update new values.
int oldLoop = d->currentLoop;
d->currentLoop = ((dura <= 0) ? 0 : (msecs / dura));
if (d->currentLoop == d->loopCount) {
//we're at the end
d->currentTime = qMax(0, dura);
d->currentLoop = qMax(0, d->loopCount - 1);
} else {
if (d->direction == Forward) {
d->currentTime = (dura <= 0) ? msecs : (msecs % dura);
} else {
d->currentTime = (dura <= 0) ? msecs : ((msecs - 1) % dura) + 1;
if (d->currentTime == dura)
--d->currentLoop;
}
}
updateCurrentTime(d->currentTime);
virtual void updateCurrentTime(int currentTime) = 0;
这里我们可以大概知道,QStyleAnimation是一个继承自QAbstractAnimation的动画,动画靠时间来进行推进,分成了loopCount等,每次更新时间调用updateCurrentTime()
接口,而具体的动画,就要看updateCurrentTime()实现是怎么样了,我们再找到QStyleAnimation的代码可以看到updateCurrentTime()的定义
void QStyleAnimation::updateCurrentTime(int)
{
if (++_skip >= _fps) {
_skip = 0;
if (target() && isUpdateNeeded())
updateTarget();
}
}
void QStyleAnimation::updateTarget()
{
QEvent event(QEvent::StyleAnimationUpdate);
event.setAccepted(false);
QCoreApplication::sendEvent(target(), &event);
if (!event.isAccepted())
stop();
}
然后我们在QPushButton中并未找到该事件的处理方法,然后在父类QWidget源码的event()函数中看到了其处理方法
case QEvent::StyleAnimationUpdate:
if (isVisible() && !window()->isMinimized()) {
event->accept();
update();
}
break;
可以看到,进行了重绘;update()就不用我说了吧,它会调用paintEvent();这样每过一段时间重绘一次就形成了动画。绘画的内容是什么呢,当然是QWindowsVistaStyle的
drawControl函数当element为CE_PushButton时,painter在该时该是怎么画的了,前面介绍的是所有调用该接口的都会有的操作不区分控件的;而在QWindowsVistaStyle的该接口中我们未找到该枚举的定义方法,向上层层追溯我们在QCommStyle中找到了定义,也就是最开始介绍的啦。
void QCommonStyle::drawControl(ControlElement element, const QStyleOption *opt,
QPainter *p, const QWidget *widget) const
{
Q_D(const QCommonStyle);
switch (element) {
case CE_PushButton:
if (const QStyleOptionButton *btn = qstyleoption_cast(opt)) {
proxy()->drawControl(CE_PushButtonBevel, btn, p, widget);
QStyleOptionButton subopt = *btn;
subopt.rect = subElementRect(SE_PushButtonContents, btn, widget);
proxy()->drawControl(CE_PushButtonLabel, &subopt, p, widget);
if (btn->state & State_HasFocus) {
QStyleOptionFocusRect fropt;
fropt.QStyleOption::operator=(*btn);
fropt.rect = subElementRect(SE_PushButtonFocusRect, btn, widget);
proxy()->drawPrimitive(PE_FrameFocusRect, &fropt, p, widget);
}
}
break;
可以看到其处理分为三步,分别是画按钮的斜面(让人感觉是一个按钮),画按钮上面的文字,画按钮的焦点;而这里的proxy()返回的是当前实例对象也就是QWindowsVistaStyle了,所以按钮具体怎样,就需要看QWindowsVistaStyle中drawControl接口对CE_PushButtonBevel、CE_PushButtonLabel的处理方法,
drawPrimitive接口对PE_FrameFocusRect的处理情况了。在QWindowsVistaStyle的drawControl对CE_PushButtonBevel的处理如下
case CE_PushButtonBevel:
if (const QStyleOptionButton *btn = qstyleoption_cast(option))
{
themeNumber = QWindowsXPStylePrivate::ButtonTheme;
partId = BP_PUSHBUTTON;
if (btn->features & QStyleOptionButton::CommandLinkButton)
partId = BP_COMMANDLINK;
bool justFlat = (btn->features & QStyleOptionButton::Flat) && !(flags & (State_On|State_Sunken));
if (!(flags & State_Enabled) && !(btn->features & QStyleOptionButton::Flat))
stateId = PBS_DISABLED;
else if (justFlat)
;
else if (flags & (State_Sunken | State_On))
stateId = PBS_PRESSED;
else if (flags & State_MouseOver)
stateId = PBS_HOT;
else if (btn->features & QStyleOptionButton::DefaultButton && (state & State_Active))
stateId = PBS_DEFAULTED;
else
stateId = PBS_NORMAL;
if (!justFlat) {
if (d->transitionsEnabled() && (btn->features & QStyleOptionButton::DefaultButton) &&
!(state & (State_Sunken | State_On)) && !(state & State_MouseOver) &&
(state & State_Enabled) && (state & State_Active))
{
QWindowsVistaAnimation *anim = qobject_cast(d->animation(styleObject(option)));
if (!anim) {
QImage startImage = createAnimationBuffer(option, widget);
QImage alternateImage = createAnimationBuffer(option, widget);
QWindowsVistaPulse *pulse = new QWindowsVistaPulse(styleObject(option));
QPainter startPainter(&startImage);
stateId = PBS_DEFAULTED;
XPThemeData theme(widget, &startPainter, themeNumber, partId, stateId, rect);
d->drawBackground(theme);
QPainter alternatePainter(&alternateImage);
theme.stateId = PBS_DEFAULTED_ANIMATING;
theme.painter = &alternatePainter;
d->drawBackground(theme);
pulse->setStartImage(startImage);
pulse->setEndImage(alternateImage);
pulse->setStartTime(QTime::currentTime());
pulse->setDuration(2000);
d->startAnimation(pulse);
anim = pulse;
}
if (anim)
anim->paint(painter, option);
else {
XPThemeData theme(widget, painter, themeNumber, partId, stateId, rect);
d->drawBackground(theme);
}
}
else {
XPThemeData theme(widget, painter, themeNumber, partId, stateId, rect);
d->drawBackground(theme);
}
}
if (btn->features & QStyleOptionButton::HasMenu) {
int mbiw = 0, mbih = 0;
XPThemeData theme(widget, 0, QWindowsXPStylePrivate::ToolBarTheme,
TP_DROPDOWNBUTTON);
if (theme.isValid()) {
const QSizeF size = theme.size() * QWindowsStylePrivate::nativeMetricScaleFactor(widget);
if (!size.isEmpty()) {
mbiw = qRound(size.width());
mbih = qRound(size.height());
}
}
QRect ir = subElementRect(SE_PushButtonContents, option, 0);
QStyleOptionButton newBtn = *btn;
newBtn.rect = QStyle::visualRect(option->direction, option->rect,
QRect(ir.right() - mbiw - 2,
option->rect.top() + (option->rect.height()/2) - (mbih/2),
mbiw + 1, mbih + 1));
proxy()->drawPrimitive(PE_IndicatorArrowDown, &newBtn, painter, widget);
}
return;
}
可以看到启用了动画,并使用d->drawBackground()画出了该时刻按钮的背景,从前面知道QStyleAnimation会触发重绘,这样就不断以不一样的参数绘制
出了按钮的动画效果,当然这里还区分了按钮有菜单的情况,对CE_PushButtonLabel的处理未找到,自己追溯上一级吧,方法类似。
这样整个过程就出来了
1.QPushButton的paintEvent中以本对象为绘图设备创建QPainter,还有QStyleOptionButton; 初始化QStyleOptionButton,调用QStyle的drawControl
2.windows平台QPushButton用的是QWindowsVistaStyle可打印QPushButton的style()返回值查看
3.QWindowsVistaStyle的drawControl()根据property(_qt_no_animaiotn)判断是否能够动画,如果能动画,创建两个Image,一个开始,一个结束
4.启用QWindowsVistaStylePrivate的startAnimation(),传入构建的QStyleAnimation。
5.QStyleAnimation通过updateCurrentTime()定时的通过QCoreApplication::sendEvent()向绘图设备对象发送StyleAnimationUpdate事件
6.QWindowsVistaStyle对按钮的背景斜面板,文字、焦点实时绘制