Qt学习笔记外观篇(二):QStyle

三种方法来重新定义Qt内置窗口部件的外观:
1)子类化QStyle或者一个预定义的风格,这种方法很好用,Qt本身就是用这种方法为它所支持的不同平台提供基于平台的外观的
2)子类化个别的窗口部件类,并且重新实现它的绘制和鼠标事件处理器。

3)Qt 样式表


QStyle类包装应用程序外观,对于应用程序的每个窗口部件,都会使用QStyle进行绘制。
QStyle API包含绘制图形元素[drawPrimitive(),drawControl(),drawComplexControl()等]函数,样式查询函数[pixelMetrics(),styleHint(),hitTest()等]。QStyle成员函数典型的带有QStyleOption对象,它包含绘制窗口部件的通用信息(例如调色板)以及特有信息(例如按钮的文字)。

下面解析如何子类化QStyle来实现自定义外观:

子类化QStyle一般是通过重新实现几个虚函数来实现的。

1)polish(QPalette& palette):通常在此函数内指定配色方案,也即配置调色板

2)polish(QWidget*)&unpolish(QWidget*):当样式应用到窗口部件时,polish(QWidget*)就会调用,从而允许我们进行最后的定制,当动态改变样式的时候,unpolish就会调用,来撤销polish的影响。polish(QWidget*)一般用做窗口部件的事件过滤器。

3)styleHint返回关于样式外观的提示

4)pixelMetric返回像素值,来影响窗口部件的绘制

5)drawPrimitive(),drawControl(),drawComplexControl()具体的执行绘制。

Qt学习笔记外观篇(二):QStyle_第1张图片

如何实现自定义样式使得左侧窗口成为右侧窗口?

首先是颜色,各个窗口部件的背景色发生了变化;然后是QSpinBox的形状发生了变化,QCheckBox的选取标志发生了变化还有就是Save&Cancel按钮的位置发生了变化。即实现:带渐变背景的圆角按钮、非传统的微调框、夸张的选取标志和“棕色画刷”背景。

类声明如下:

#include 
class BronzeStyle : public QWindowsStyle
{
    Q_OBJECT
public:
    BronzeStyle(){}
    void polish(QPalette& palette);
    void polish(QWidget* widget);
    void unpolish(QWidget* widget);
    int  styleHint(StyleHint hint, const QStyleOption *opt, const QWidget *widget=0, QStyleHintReturn *returnData=0) const;
    int  pixelMetric(PixelMetric pm, const QStyleOption *option, const QWidget *widget=0) const;
    void drawPrimitive(PrimitiveElement which,const QStyleOption* option,QPainter* painter,const QWidget* widget=0)const;
    void drawComplexControl(ComplexControl which,const QStyleOptionComplex* option,QPainter* painter,const QWidget* widget=0)const;
    QRect subControlRect(ComplexControl cc, const QStyleOptionComplex *opt, SubControl sc, const QWidget *w=0) const;
public slots:
    QIcon standardIconImplementation(StandardPixmap standardIcon, const QStyleOption *option, const QWidget *widget=0) const;
private:
    void drawBronzeFrame(const QStyleOption* option,QPainter* painter)const;
    void drawBronzeBevel(const QStyleOption* option,QPainter* painter)const;
    void drawBronzeCheckBoxIndicator(const QStyleOption* option,QPainter* painter)const;
    void drawBronzeSpinBoxButton(SubControl which,const QStyleOptionComplex* option,QPainter* painter)const;

};

现在开始实现第一步:颜色配置

void BronzeStyle::polish(QPalette &palette)
{
    QPixmap backgroundImage(":/images/background.png");
    QColor bronze(207,155,95);
    QColor veryLightBlue(239,239,247);
    QColor lightBlue(223,223,239);
    QColor darkBlue(95,95,191);

    palette=QPalette(bronze);
    palette.setBrush(QPalette::Window,backgroundImage);
    palette.setBrush(QPalette::BrightText, Qt::white);
    palette.setBrush(QPalette::Base, veryLightBlue);
    palette.setBrush(QPalette::AlternateBase, lightBlue);
    palette.setBrush(QPalette::Highlight, darkBlue);
    palette.setBrush(QPalette::Disabled, QPalette::Highlight,Qt::darkGray);
}
根据前篇中对QPalette的介绍,我们很容易明白这里的设置。

第二步,来完成查询函数,提供绘制窗口部件所需要的信息:

int BronzeStyle::styleHint(StyleHint which, const QStyleOption *option, const QWidget *widget,
                           QStyleHintReturn *returnData) const
{
    switch(which){
    case SH_DialogButtonLayout:
        return int(QDialogButtonBox::MacLayout);
    case SH_EtchDisabledText:
        return int(true);
    case SH_DialogButtonBox_ButtonsHaveIcons:
        return int(true);
    case SH_UnderlineShortcut:
        return int(false);
    default:
        return QWindowsStyle::styleHint(which,option,widget,returnData);
    }
}

int BronzeStyle::pixelMetric(PixelMetric which, const QStyleOption *option, const QWidget *widget) const
{
    switch(which){
    case PM_ButtonDefaultIndicator:
        return 0;
    case PM_IndicatorHeight:
    case PM_IndicatorWidth:
        return 16;
    case PM_CheckBoxLabelSpacing:
        return 8;
    case PM_DefaultFrameWidth:
        return 2;
    default:
        return QWindowsStyle::pixelMetric(which,option,widget);
    }
}

其中的styleHint函数完成的样式外观的提示,对于SH_DialogButtonLayout,其返回MacLayout,从而实现OK键在Cancel的右侧;pixelMetric函数返回关于大小的提示:按钮Indicator值为0,checkbox指示器为16*16的正方形,checkbox指示器与文本的距离为8,FramwWidth为2。

第三步实现绘制:

QStyle绘制函数的参数形式一般是

1)一个enum成员指定需要绘制的图形元素

2)一个QStyleOption指定这个图形元素怎么画和在哪儿画

3)一个QPainter

4)一个可选参数QWidget,指定了图形元素是在哪一个窗口部件上绘制

void BronzeStyle::drawPrimitive(PrimitiveElement which, const QStyleOption *option,
                                QPainter *painter, const QWidget *widget) const
{
    switch(which){
    case PE_IndicatorCheckBox:
        drawBronzeCheckBoxIndicator(option,painter);
        break;
    case PE_PanelButtonCommand:
        drawBronzeBevel(option,painter);
        break;
    case PE_Frame:
        drawBronzeFrame(option,painter);
        break;
    case PE_FrameDefaultButton:
        break;
    default:
        QWindowsStyle::drawPrimitive(which,option,painter,widget);
    }
}

drawPrimitive()绘制“基本的”用户界面元素,这些元素会被几个窗口部件使用。此处,我们要实现夸张的选择指示器和渐变的按钮,所以要用自定义的绘制函数实现对PE_IndicatorCheckBox和PE_PanelButtonCommand的绘制;

void BronzeStyle::drawComplexControl(ComplexControl which, const QStyleOptionComplex *option,
                                     QPainter *painter, const QWidget *widget) const
{
    if(which==CC_SpinBox){
        drawBronzeSpinBoxButton(SC_SpinBoxDown,option,painter);
        drawBronzeSpinBoxButton(SC_SpinBoxUp,option,painter);
        QRect rect=subControlRect(CC_SpinBox,option,SC_SpinBoxEditField).adjusted(-1,0,+1,0);
        painter->setPen(QPen(option->palette.mid(),1.0));
        painter->drawLine(rect.topLeft(),rect.bottomLeft());
        painter->drawLine(rect.topRight(),rect.bottomRight());
    }else{
        return QWindowsStyle::drawComplexControl(which,option,painter,widget);
    }
}

因为drawComplexControl函数绘制多重辅助控制器窗口部件而且我们需要非传统的SpinBox,所以此处实现对CC_SpinBox的自定义绘制:首先绘制两个按钮,然后在文本框两侧绘制两条线。注意此函数中调用了一个叫subControlRect的函数,此函数用于确定辅助控制器窗口部件的位置(所谓的辅助控制器即为窗口部件的某一组成部分,例如checkbox的指示器)。处理鼠标事件的时候也用它查找被单击的辅助控制器窗口部件。

QRect BronzeStyle::subControlRect(ComplexControl whichControl, const QStyleOptionComplex *option,
                                  SubControl whichSubControl, const QWidget *widget) const
{
    if(whichControl==CC_SpinBox){
        int frameWidth=pixelMetric(PM_DefaultFrameWidth,option,widget);
        int buttonWidth=16;
        switch(whichSubControl){
        case SC_SpinBoxFrame:
            return option->rect;
        case SC_SpinBoxEditField:
            return option->rect.adjusted(+buttonWidth,+frameWidth,-buttonWidth,-frameWidth);
        case SC_SpinBoxDown:
            return visualRect(option->direction,option->rect, QRect(option->rect.x(),option->rect.y(),
                                                       buttonWidth,option->rect.height()));
        case SC_SpinBoxUp:
            return visualRect(option->direction,option->rect, QRect(option->rect.right()-buttonWidth,
                                         option->rect.y(), buttonWidth,option->rect.height()));
        default:
            return QRect();
        }
    }else{
        return QWindowsStyle::subControlRect(whichControl,option,whichSubControl,widget);
    }

}

此处重新实现了对SpinBox的各个子部件的定位:左侧Down按钮(16*height),中间EditField,右侧Up按钮(16*height)。

上述的绘制函数完成了绘制任务的分派,下面是具体的绘制过程:

void BronzeStyle::drawBronzeFrame(const QStyleOption *option, QPainter *painter) const
{
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing,true);

    painter->setPen(QPen(option->palette.foreground(),1.0));
    painter->drawRect(option->rect.adjusted(+1,+1,-1,-1));
    painter->restore();
}

void BronzeStyle::drawBronzeBevel(const QStyleOption *option, QPainter *painter) const
{
    QColor buttonColor=option->palette.button().color();
    int coeff=(option->state&State_MouseOver)?115:105;

    QLinearGradient gradient(0,0,0,option->rect.height());
    gradient.setColorAt(0.0,option->palette.light().color());
    gradient.setColorAt(0.2,buttonColor.lighter(coeff));
    gradient.setColorAt(0.8,buttonColor.darker(coeff));
    gradient.setColorAt(1.0,option->palette.dark().color());

    double penWidth=1.0;
    if(const QStyleOptionButton* buttonOpt=qstyleoption_cast(option)){
        if(buttonOpt->features&QStyleOptionButton::DefaultButton)
            penWidth=2.0;
    }
    QRect roundRect=option->rect.adjusted(+1,+1,-1,-1);
    if(!roundRect.isValid())
        return;

    int diameter=12;
    int cx=100*diameter/roundRect.width();
    int cy=100*diameter/roundRect.height();

    painter->save();
    painter->setPen(Qt::NoPen);
    painter->setBrush(gradient);
    painter->drawRoundRect(roundRect,cx,cy);

    if(option->state&(State_On|State_Sunken)){
        QColor slightlyOpaqueBlack(0,0,0,63);
        painter->setBrush(slightlyOpaqueBlack);
        painter->drawRoundRect(roundRect,cx,cy);
    }

    painter->setRenderHint(QPainter::Antialiasing,true);
    painter->setPen(QPen(option->palette.foreground(),penWidth));
    painter->setBrush(Qt::NoBrush);
    painter->drawRoundRect(roundRect,cx,cy);
    painter->restore();
}

注意对按钮圆角和渐变效果的实现:首先获得需要圆角的绘制区域,然后使用渐变颜色填充绘制,最后绘制边框。其中涉及到的一些变化是:1)鼠标是否放在了按钮上?如果是,那么颜色*coeff。2)按钮是否被按下?如果是,那么使用slightlyOpaqueBlack填充;3)是否是默认按钮?如果是,那么边框加粗;

void BronzeStyle::drawBronzeSpinBoxButton(SubControl which, const QStyleOptionComplex *option,
                                          QPainter *painter) const
{
    PrimitiveElement arrow=PE_IndicatorArrowLeft;
    QRect buttonRect=option->rect;
    if((which==SC_SpinBoxUp)!=(option->direction==Qt::RightToLeft)){
        arrow=PE_IndicatorArrowRight;
        buttonRect.translate(buttonRect.width()/2,0);
    }
    buttonRect.setWidth((buttonRect.width()+1)/2);

    QStyleOption buttonOpt(*option);

    painter->save();
    painter->setClipRect(buttonRect,Qt::IntersectClip);
    if(!(option->activeSubControls&which))
        buttonOpt.state&=~(State_MouseOver|State_On|State_Sunken);
    drawBronzeBevel(&buttonOpt,painter);

    QStyleOption arrowOpt(buttonOpt);
    arrowOpt.rect=subControlRect(CC_SpinBox,option,which).adjusted(+3,+3,-3,-3);
    if(arrowOpt.rect.isValid())
        drawPrimitive(arrow,&arrowOpt,painter);
    painter->restore();
}

此为绘制SpinBox按钮的过程:首先确定按钮arrow图像,然后先绘制好按钮的panel(渐变和圆角),然后再绘制arrow图像

void BronzeStyle::drawBronzeCheckBoxIndicator(const QStyleOption *option, QPainter *painter) const
{
    painter->save();
    painter->setRenderHint(QPainter::Antialiasing,true);
    if(option->state&State_MouseOver){
        painter->setBrush(option->palette.alternateBase());
    }else{
        painter->setBrush(option->palette.base());
    }
    painter->drawRoundRect(option->rect.adjusted(+1,+1,-1,-1));
    if(option->state&(State_On|State_NoChange)){
        QPixmap pixmap;
        if(!(option->state&State_Enabled)){
            pixmap.load(":/images/checkmark-disabled.png");
        }else if(option->state&State_NoChange){
            pixmap.load(":/images/checkmark-partial.png");
        }else{
            pixmap.load(":/images/checkmark.png");
        }
        QRect pixmapRect=pixmap.rect().translated(option->rect.topLeft()).translated(+2,-6);
        QRect painterRect=visualRect(option->direction,option->rect,pixmapRect);
        if(option->direction==Qt::RightToLeft){
            painter->scale(-1.0,+1.0);
            painterRect.moveLeft(-painterRect.right()-1);
        }
        painter->drawPixmap(painterRect,pixmap);
    }
    painter->restore();
}

此为checkbox的指示器的绘制过程,首先绘制指示器的边框,然后根据checkbox是否选中,绘制选中的图像(此处为“对勾号”)。


QStyle绘制的整体流程框架就是如此,下面是一些补充:

void BronzeStyle::polish(QWidget *widget)
{
    if(qobject_cast(widget)||qobject_cast(widget))
        widget->setAttribute(Qt::WA_Hover,true);
}

void BronzeStyle::unpolish(QWidget *widget)
{
    if(qobject_cast(widget)||qobject_cast(widget))
        widget->setAttribute(Qt::WA_Hover,false);
}

使得鼠标进出窗口部件都产生绘制事件。

应用:QApplication::setStyle(new BronzeStyle);

这里的代码约300行,然而要开发一种功能齐全的自定义样式是一项很大的工程,大约需要3000~5000行代码,所以对于轻量级的改动,我们一般不使用此方法,但是此框架是Qt窗口部件的基本的绘制原理。



你可能感兴趣的:(Qt)